Comparing version 0.8.1 to 0.8.2
@@ -1,8 +0,11 @@ | ||
// Bookshelf.js 0.8.1 | ||
// --------------- | ||
// (c) 2014 Tim Griesser | ||
// Bookshelf may be freely distributed under the MIT license. | ||
// For all details and documentation: | ||
// http://bookshelfjs.org | ||
module.exports = require('./lib') | ||
/** | ||
* (c) 2014 Tim Griesser | ||
* Bookshelf may be freely distributed under the MIT license. | ||
* For all details and documentation: | ||
* http://bookshelfjs.org | ||
* | ||
* version 0.8.1 | ||
* | ||
*/ | ||
module.exports = require('./lib/bookshelf') |
## How to contribute to Bookshelf.js | ||
* Before sending a pull request for a feature or bug fix, be sure to have | ||
[tests](https://github.com/tgriesser/bookshelf/tree/master/test). | ||
[tests](https://github.com/tgriesser/bookshelf/tree/master/test). | ||
* Use the same coding style as the rest of the | ||
[codebase](https://github.com/tgriesser/bookshelf/blob/master/bookshelf.js). | ||
[codebase](https://github.com/tgriesser/bookshelf/blob/master/src/bookshelf.js). | ||
* Make changes in the /src directory, running "npm run dev" which will kick off | ||
transpilation from ES6 in the background | ||
* Files in the `/browser` folder are built automatically. You don't need to edit them. | ||
@@ -13,5 +16,161 @@ | ||
### Development Environment Setup | ||
You'll need to have `git` installed obviously. Begin by forking the [main | ||
repository](https://github.com/tgriesser/bookshelf) and then getting the code from your forked copy: | ||
```sh | ||
$ git clone git@github.com:yourusername/bookshelf.git | ||
``` | ||
Afterwards go to the bookshelf directory that was just created and install the dependencies: | ||
```sh | ||
$ npm install | ||
``` | ||
At this point the only thing missing are the databases that will be used for running some of the tests of the automated | ||
test suite. | ||
There are two options for setting up this part. The first one is to change some configuration options of the database | ||
servers and the other is to use a config file in case you already have your servers configured and don't want to change | ||
any of their config files. The first two sections below deal with the first option and then there are instructions on | ||
how to use the other option. | ||
#### MySQL | ||
You can install [MySQL](https://www.mysql.com/) easily on most linux distros by using their package manager. With Ubuntu | ||
this should do it: | ||
```sh | ||
$ sudo apt-get install mysql-server mysql-client | ||
``` | ||
On OSX you can download a disk image directly from the [MySQL Downloads page](http://dev.mysql.com/downloads/mysql/), or | ||
use one of the popular package managers like [homebrew](http://brew.sh/) or [MacPorts](https://www.macports.org/). | ||
To run the test suite you will need to make sure it is possible to connect as the user `root` without the need for a | ||
password. | ||
It is strongly recommended that you use the command line `mysql` client to access your MySQL instance since there can be | ||
problems connecting as the root user with some graphical clients like `phpMyAdmin`. To check if you can connect as root | ||
without needing a password use the following command: | ||
```sh | ||
$ mysql -u root | ||
``` | ||
If you see an error like: | ||
```sh | ||
ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: NO) | ||
``` | ||
that means you can't login as root without a password. If you do know the root user's password just login with the known | ||
password like this: | ||
```sh | ||
$ mysql -u root -p | ||
``` | ||
and enter the password when asked. Then just set an empty password for root like so: | ||
```SQL | ||
USE mysql; | ||
UPDATE user SET password = "" WHERE User = "root"; | ||
FLUSH PRIVILEGES; | ||
QUIT; | ||
``` | ||
Note that you'll probably need to set the password to `NULL` instead of an empty string in MySQL versions 5.5 and older. | ||
The above example should work with versions 5.6 and newer. | ||
If you have forgotten the root password you'll need to take some extra steps to reset it. Take a look at | ||
[this Stack Overflow answer](http://stackoverflow.com/a/7825212/504930) for further details. | ||
#### PostgreSQL | ||
You can install [PostgreSQL](http://www.postgresql.org/) easily on most linux distros by using their package manager. | ||
With Ubuntu this should do it: | ||
```sh | ||
$ sudo apt-get install postgresql postgresql-client | ||
``` | ||
On OSX the easiest way is probably by using [PosgresApp](http://postgresapp.com/). It should also be available to | ||
install via [homebrew](http://brew.sh/) or [MacPorts](https://www.macports.org/) if you prefer. | ||
In the case of PostgreSQL the requirement is to be able to connect as the `postgres` user on localhost also without the | ||
need for a password. This can be achieved by editing or adding the following line in the `pg_hba.conf` file: | ||
``` | ||
host all all 127.0.0.1/32 trust | ||
``` | ||
This file can be found in `/etc/postgresql/9.4/main/` on most linux systems. The `9.4` part could be different depending | ||
on the version that is available in your distro. On OSX the location of this file will depend on the installation method | ||
chosen, but for the recommended PostgresApp install it will be in `/Users/[yourusername]/Library/Application | ||
Support/Postgres/var-9.3/`. Again, the `var-9.3` part may be different depending on the version you installed. | ||
The `trust` in the example above tells the locally running PostgreSQL server to ignore user passwords and always grant | ||
access on clients connecting locally. Do not use this setting in a production environment. | ||
After editing the `pg_hba.conf` file you'll need to restart the PostgreSQL server for the changes to take effect. | ||
#### Using a config file | ||
If you don't want to go to the trouble of performing the changes explained in the previous two sections you can instead | ||
use a config file that tells the test suite about your database setup. | ||
The tests will look for a `BOOKSHELF_TEST` environment variable that points to a `config.js` file with the connection | ||
details for each database server. This file must not be the same database config file you use for any other application, | ||
otherwise you risk data loss in that application. | ||
Example config file: | ||
```javascript | ||
module.exports = { | ||
mysql: { | ||
database: 'bookshelf_test', | ||
user: 'root', | ||
encoding: 'utf8' | ||
}, | ||
postgres: { | ||
user: 'myusername', | ||
database: 'bookshelf_test', | ||
password: 'secretpassword', | ||
host: 'localhost', | ||
port: 5432, | ||
charset: 'utf8', | ||
ssl: false | ||
}, | ||
sqlite3: { | ||
filename: ':memory:' | ||
} | ||
}; | ||
``` | ||
This file can be placed anywhere on your system and can have any name that you like, as long as the environment variable | ||
is pointing correctly to it. For convenience you can put it in your home directory and add the following line to your | ||
`.bashrc` or `.zshrc`: | ||
``` | ||
export BOOKSHELF_TEST='/home/myusername/.bookshelf_config.js' | ||
``` | ||
#### Database creation | ||
After having ensured the test suite can access both database servers just create a new database on each that will be | ||
used exclusively by Bookshelf.js: | ||
```SQL | ||
CREATE DATABASE bookshelf_test; | ||
``` | ||
### Running the Tests | ||
The test suite requires you to create a [MySQL](https://www.mysql.com/) and [Postgres](http://www.postgresql.org/) database named `bookshelf_test`. | ||
The test suite requires that both MySQL and PostgreSQL servers have a database named `bookshelf_test`. See the sections | ||
above for further instructions. | ||
@@ -22,2 +181,4 @@ Once you have done that, you can run the tests: | ||
$ npm test | ||
``` | ||
``` | ||
Always make sure all the tests are passing before sending a pull request. |
@@ -5,23 +5,28 @@ // Base Collection | ||
// All exernal dependencies required in this scope. | ||
var _ = require('lodash'); | ||
var inherits = require('inherits'); | ||
'use strict'; | ||
var _ = require('lodash'); | ||
var inherits = require('inherits'); | ||
// All components that need to be referenced in this scope. | ||
var Events = require('./events'); | ||
var Promise = require('./promise'); | ||
var Events = require('./events'); | ||
var Promise = require('./promise'); | ||
var ModelBase = require('./model'); | ||
var array = []; | ||
var push = array.push; | ||
var slice = array.slice; | ||
var array = []; | ||
var slice = array.slice; | ||
var splice = array.splice; | ||
var noop = _.noop; | ||
/** | ||
* @class | ||
*/ | ||
function CollectionBase(models, options) { | ||
if (options) _.extend(this, _.pick(options, collectionProps)); | ||
this._reset(); | ||
this.initialize.apply(this, arguments) | ||
this.initialize.apply(this, arguments); | ||
if (!_.isFunction(this.model)) { | ||
throw new Error('A valid `model` constructor must be defined for all collections.'); | ||
} | ||
if (models) this.reset(models, _.extend({silent: true}, options)); | ||
if (models) this.reset(models, _.extend({ silent: true }, options)); | ||
} | ||
@@ -31,49 +36,125 @@ inherits(CollectionBase, Events); | ||
// List of attributes attached directly from the constructor's options object. | ||
var collectionProps = ['model', 'Model', 'comparator']; | ||
// | ||
// RE: 'relatedData' | ||
// It's okay for two `Collection`s to share a `Relation` instance. | ||
// `relatedData` does not mutate itself after declaration. This is only | ||
// here because `clone` needs to duplicate this property. It should not | ||
// be documented as a valid argument for consumer code. | ||
var collectionProps = ['model', 'comparator', 'relatedData']; | ||
// Copied over from Backbone. | ||
var setOptions = {add: true, remove: true, merge: true}; | ||
var addOptions = {add: true, remove: false}; | ||
var setOptions = { add: true, remove: true, merge: true }; | ||
var addOptions = { add: true, remove: false }; | ||
CollectionBase.prototype.initialize = function() {}; | ||
/** | ||
* @method CollectionBase#initialize | ||
* @description | ||
* Custom initialization function. | ||
* @see Collection | ||
*/ | ||
CollectionBase.prototype.initialize = noop; | ||
// The `tableName` on the associated Model, used in relation building. | ||
CollectionBase.prototype.tableName = function() { | ||
/** | ||
* @method | ||
* @private | ||
* @description | ||
* The `tableName` on the associated Model, used in relation building. | ||
* @returns {string} The {@link Model#tableName tableName} of the associated model. | ||
*/ | ||
CollectionBase.prototype.tableName = function () { | ||
return _.result(this.model.prototype, 'tableName'); | ||
}; | ||
// The `idAttribute` on the associated Model, used in relation building. | ||
CollectionBase.prototype.idAttribute = function() { | ||
/** | ||
* @method | ||
* @private | ||
* @description | ||
* The `idAttribute` on the associated Model, used in relation building. | ||
* @returns {string} The {@link Model#idAttribute idAttribute} of the associated model. | ||
*/ | ||
CollectionBase.prototype.idAttribute = function () { | ||
return this.model.prototype.idAttribute; | ||
}; | ||
CollectionBase.prototype.toString = function() { | ||
CollectionBase.prototype.toString = function () { | ||
return '[Object Collection]'; | ||
}; | ||
CollectionBase.prototype.serialize = function(options) { | ||
return this.map(function(model){ | ||
return model.toJSON(options); | ||
/** | ||
* @method | ||
* @description | ||
* | ||
* Return a raw array of the collection's {@link ModelBase#attributes | ||
* attributes} for JSON stringification. If the {@link Model models} have any | ||
* relations defined, this will also call {@link ModelBase ModelBase#toJSON} on | ||
* each of the related objects, and include them on the object unless | ||
* `{shallow: true}` is passed as an option. | ||
* | ||
* `serialize` is called internally by {@link CollectionBase#toJSON toJSON}. | ||
* Override this function if you want to customize its output. | ||
* | ||
* @param {Object=} options | ||
* @param {bool} [options.shallow=false] Exclude relations. | ||
* @param {bool} [options.omitPivot=false] Exclude pivot values. | ||
* @returns {Object} Serialized model as a plain object. | ||
*/ | ||
CollectionBase.prototype.serialize = function (options) { | ||
return this.map(function (model) { | ||
return model.toJSON(options); | ||
}); | ||
} | ||
}; | ||
// The JSON representation of a Collection is an array of the | ||
// models' attributes. | ||
CollectionBase.prototype.toJSON = function(options) { | ||
return this.serialize(options) | ||
/** | ||
* @method | ||
* @description | ||
* | ||
* Called automatically by {@link | ||
* https://developer.mozilla.org/en-US/docs/Glossary/JSON#toJSON()_method | ||
* `JSON.stringify`}. To customize serialization, override {@link | ||
* CollectionBase#serialize serialize}. | ||
* | ||
* @param {options} Options passed to {@link CollectionBase#serialize}. | ||
*/ | ||
CollectionBase.prototype.toJSON = function (options) { | ||
return this.serialize(options); | ||
}; | ||
// A simplified version of Backbone's `Collection#set` method, | ||
// removing the comparator, and getting rid of the temporary model creation, | ||
// since there's *no way* we'll be getting the data in an inconsistent | ||
// form from the database. | ||
CollectionBase.prototype.set = function(models, options) { | ||
/** | ||
* @method | ||
* @description | ||
* | ||
* A simplified version of Backbone's `Collection#set` method, removing the | ||
* comparator, and getting rid of the temporary model creation, since there's | ||
* *no way* we'll be getting the data in an inconsistent form from the database. | ||
* | ||
* The set method performs a "smart" update of the collection with the passed | ||
* list of models. If a model in the list isn't yet in the collection it will be | ||
* added; if the model is already in the collection its attributes will be | ||
* merged; and if the collection contains any models that aren't present in the | ||
* list, they'll be removed. If you'd like to customize the behavior, you can | ||
* disable it with options: `{add: false}`, `{remove: false}`, or | ||
* `{merge: false}`. | ||
* | ||
* @example | ||
* | ||
* var vanHalen = new bookshelf.Collection([eddie, alex, stone, roth]); | ||
* vanHalen.set([eddie, alex, stone, hagar]); | ||
* | ||
* @param {Model[]|Object[]} models Array of models or raw attribute objects. | ||
* @param {Object=} options See description. | ||
* @returns {Collection} Self, this method is chainable. | ||
*/ | ||
CollectionBase.prototype.set = function (models, options) { | ||
options = _.defaults({}, options, setOptions); | ||
if (options.parse) models = this.parse(models, options); | ||
if (!_.isArray(models)) models = models ? [models] : []; | ||
var i, l, id, model, attrs, existing; | ||
var i, l, id, model, attrs; | ||
var at = options.at; | ||
var targetModel = this.model; | ||
var toAdd = [], toRemove = [], modelMap = {}; | ||
var add = options.add, merge = options.merge, remove = options.remove; | ||
var toAdd = [], | ||
toRemove = [], | ||
modelMap = {}; | ||
var add = options.add, | ||
merge = options.merge, | ||
remove = options.remove; | ||
var order = add && remove ? [] : false; | ||
@@ -93,3 +174,4 @@ | ||
// optionally merge it into the existing model. | ||
if (existing = this.get(id)) { | ||
var existing = this.get(id); | ||
if (existing) { | ||
if (remove) { | ||
@@ -108,6 +190,2 @@ modelMap[existing.cid] = true; | ||
toAdd.push(model); | ||
// Listen to added models' events, and index models for lookup by | ||
// `id` and by `cid`. | ||
model.on('all', this._onModelEvent, this); | ||
this._byId[model.cid] = model; | ||
@@ -128,3 +206,3 @@ if (model.id != null) this._byId[model.id] = model; | ||
// See if sorting is needed, update `length` and splice in new models. | ||
if (toAdd.length || (order && order.length)) { | ||
if (toAdd.length || order && order.length) { | ||
this.length += toAdd.length; | ||
@@ -154,4 +232,9 @@ if (at != null) { | ||
// Prepare a model or hash of attributes to be added to this collection. | ||
CollectionBase.prototype._prepareModel = function(attrs, options) { | ||
/** | ||
* @method | ||
* @private | ||
* @description | ||
* Prepare a model or hash of attributes to be added to this collection. | ||
*/ | ||
CollectionBase.prototype._prepareModel = function (attrs, options) { | ||
if (attrs instanceof ModelBase) return attrs; | ||
@@ -161,40 +244,106 @@ return new this.model(attrs, options); | ||
// Run "Promise.map" over the models | ||
CollectionBase.prototype.mapThen = function(iterator, context) { | ||
/** | ||
* @method | ||
* @private | ||
* @description | ||
* Run "Promise.map" over the models | ||
*/ | ||
CollectionBase.prototype.mapThen = function (iterator, context) { | ||
return Promise.bind(context).thenReturn(this.models).map(iterator); | ||
}; | ||
// Convenience method for invoke, returning a `Promise.all` promise. | ||
CollectionBase.prototype.invokeThen = function() { | ||
/** | ||
* @method | ||
* @description | ||
* Shortcut for calling `Promise.all` around a {@link Collection#invoke}, this | ||
* will delegate to the collection's `invoke` method, resolving the promise with | ||
* an array of responses all async (and sync) behavior has settled. Useful for | ||
* bulk saving or deleting models: | ||
* | ||
* @param {string} method The {@link Model model} method to invoke. | ||
* @param {...mixed} arguments Arguments to `method`. | ||
* @returns {Promise<mixed[]>} | ||
* Promise resolving to array of results from invocation. | ||
*/ | ||
CollectionBase.prototype.invokeThen = function () { | ||
return Promise.all(this.invoke.apply(this, arguments)); | ||
}; | ||
// Run "reduce" over the models in the collection. | ||
CollectionBase.prototype.reduceThen = function(iterator, initialValue, context) { | ||
/** | ||
* @method | ||
* @description | ||
* Run "reduce" over the models in the collection. | ||
* @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce | MDN `Array.prototype.reduce` reference.} | ||
* @param {Function} iterator | ||
* @param {mixed} initialValue | ||
* @param {Object} Bound to `this` in the `iterator` callback. | ||
* @returns {Promise<mixed[]>} | ||
* Promise resolving to array of results from invocation. | ||
* | ||
*/ | ||
CollectionBase.prototype.reduceThen = function (iterator, initialValue, context) { | ||
return Promise.bind(context).thenReturn(this.models).reduce(iterator, initialValue).bind(); | ||
}; | ||
CollectionBase.prototype.fetch = function() { | ||
CollectionBase.prototype.fetch = function () { | ||
return Promise.rejected('The fetch method has not been implemented'); | ||
}; | ||
// Add a model, or list of models to the set. | ||
CollectionBase.prototype.add = function(models, options) { | ||
return this.set(models, _.extend({merge: false}, options, addOptions)); | ||
/** | ||
* @method | ||
* @description | ||
* | ||
* Add a {@link Model model} (or an array of models) to the collection, You may | ||
* also pass raw attributes objects, and have them be vivified as instances of | ||
* the model. Pass `{at: index}` to splice the model into the collection at the | ||
* specified `index`. If you're adding models to the collection that are already | ||
* in the collection, they'll be ignored, unless you pass `{merge: true}`, in | ||
* which case their {@link Model#attributes attributes} will be merged into the | ||
* corresponding models. | ||
* | ||
* *Note that adding the same model (a model with the same id) to a collection | ||
* more than once is a no-op.* | ||
* | ||
* @example | ||
* | ||
* var ships = new bookshelf.Collection; | ||
* | ||
* ships.add([ | ||
* {name: "Flying Dutchman"}, | ||
* {name: "Black Pearl"} | ||
* ]); | ||
* | ||
* @param {Object[]|Model[]} models Array of models or raw attribute objects. | ||
* @param {Object=} options See description. | ||
* @returns {Collection} Self, this method is chainable. | ||
*/ | ||
CollectionBase.prototype.add = function (models, options) { | ||
return this.set(models, _.extend({ merge: false }, options, addOptions)); | ||
}; | ||
// Remove a model, or a list of models from the set. | ||
CollectionBase.prototype.remove = function(models, options) { | ||
/** | ||
* @method | ||
* @description | ||
* | ||
* Remove a {@link Model model} (or an array of models) from the collection, | ||
* but does not remove the model from the database, use the model's {@link | ||
* Model#destroy destroy} method for this. | ||
* | ||
* @param {Model|Model[]} models The model, or models, to be removed. | ||
* @param {Object} options | ||
* @returns {Model|Model[]} The same value passed as `models` argument. | ||
*/ | ||
CollectionBase.prototype.remove = function (models, options) { | ||
var singular = !_.isArray(models); | ||
models = singular ? [models] : _.clone(models); | ||
options || (options = {}); | ||
var i, l, index, model; | ||
for (i = 0, l = models.length; i < l; i++) { | ||
model = models[i] = this.get(models[i]); | ||
options = options || {}; | ||
var i = -1; | ||
while (++i < models.length) { | ||
var model = models[i] = this.get(models[i]); | ||
if (!model) continue; | ||
delete this._byId[model.id]; | ||
delete this._byId[model.cid]; | ||
index = this.indexOf(model); | ||
var index = this.indexOf(model); | ||
this.models.splice(index, 1); | ||
this.length--; | ||
this.length = this.length - 1; | ||
if (!options.silent) { | ||
@@ -204,3 +353,2 @@ options.index = index; | ||
} | ||
this._removeReference(model); | ||
} | ||
@@ -210,14 +358,21 @@ return singular ? models[0] : models; | ||
// When you have more items than you want to add or remove individually, | ||
// you can reset the entire set with a new list of models, without firing | ||
// any granular `add` or `remove` events. Fires `reset` when finished. | ||
// Useful for bulk operations and optimizations. | ||
CollectionBase.prototype.reset = function(models, options) { | ||
/** | ||
* @method | ||
* @description | ||
* | ||
* Adding and removing models one at a time is all well and good, but sometimes | ||
* you have so many models to change that you'd rather just update the | ||
* collection in bulk. Use `reset` to replace a collection with a new list of | ||
* models (or attribute hashes). Calling `collection.reset()` without passing | ||
* any models as arguments will empty the entire collection. | ||
* | ||
* @param {Object[]|Model[]} Array of models or raw attribute objects. | ||
* @param {Object} options See {@link CollectionBase#add add}. | ||
* @returns {Model[]} Array of models. | ||
*/ | ||
CollectionBase.prototype.reset = function (models, options) { | ||
options = options || {}; | ||
for (var i = 0, l = this.models.length; i < l; i++) { | ||
this._removeReference(this.models[i]); | ||
} | ||
options.previousModels = this.models; | ||
this._reset(); | ||
models = this.add(models, _.extend({silent: true}, options)); | ||
models = this.add(models, _.extend({ silent: true }, options)); | ||
if (!options.silent) this.trigger('reset', this, options); | ||
@@ -227,9 +382,18 @@ return models; | ||
// Add a model to the end of the collection. | ||
CollectionBase.prototype.push = function(model, options) { | ||
return this.add(model, _.extend({at: this.length}, options)); | ||
/** | ||
* @method | ||
* @description | ||
* Add a model to the end of the collection. | ||
* @returns {Collection} Self, this method is chainable. | ||
*/ | ||
CollectionBase.prototype.push = function (model, options) { | ||
return this.add(model, _.extend({ at: this.length }, options)); | ||
}; | ||
// Remove a model from the end of the collection. | ||
CollectionBase.prototype.pop = function(options) { | ||
/** | ||
* @method | ||
* @description | ||
* Remove a model from the end of the collection. | ||
*/ | ||
CollectionBase.prototype.pop = function (options) { | ||
var model = this.at(this.length - 1); | ||
@@ -240,9 +404,17 @@ this.remove(model, options); | ||
// Add a model to the beginning of the collection. | ||
CollectionBase.prototype.unshift = function(model, options) { | ||
return this.add(model, _.extend({at: 0}, options)); | ||
/** | ||
* @method | ||
* @description | ||
* Add a model to the beginning of the collection. | ||
*/ | ||
CollectionBase.prototype.unshift = function (model, options) { | ||
return this.add(model, _.extend({ at: 0 }, options)); | ||
}; | ||
// Remove a model from the beginning of the collection. | ||
CollectionBase.prototype.shift = function(options) { | ||
/** | ||
* @method | ||
* @description | ||
* Remove a model from the beginning of the collection. | ||
*/ | ||
CollectionBase.prototype.shift = function (options) { | ||
var model = this.at(0); | ||
@@ -253,9 +425,25 @@ this.remove(model, options); | ||
// Slice out a sub-array of models from the collection. | ||
CollectionBase.prototype.slice = function() { | ||
/** | ||
* @method | ||
* @description | ||
* Slice out a sub-array of models from the collection. | ||
*/ | ||
CollectionBase.prototype.slice = function () { | ||
return slice.apply(this.models, arguments); | ||
}; | ||
// Get a model from the set by id. | ||
CollectionBase.prototype.get = function(obj) { | ||
/** | ||
* @method | ||
* @description | ||
* | ||
* Get a model from a collection, specified by an {@link Model#id id}, a {@link | ||
* Model#cid cid}, or by passing in a {@link Model model}. | ||
* | ||
* @example | ||
* | ||
* var book = library.get(110); | ||
* | ||
* @returns {Model} The model, or `undefined` if it is not in the collection. | ||
*/ | ||
CollectionBase.prototype.get = function (obj) { | ||
if (obj == null) return void 0; | ||
@@ -265,12 +453,22 @@ return this._byId[obj.id] || this._byId[obj.cid] || this._byId[obj]; | ||
// Get the model at the given index. | ||
CollectionBase.prototype.at = function(index) { | ||
/** | ||
* @method | ||
* @description | ||
* Get a model from a collection, specified by index. Useful if your collection | ||
* is sorted, and if your collection isn't sorted, `at` will still retrieve | ||
* models in insertion order. | ||
*/ | ||
CollectionBase.prototype.at = function (index) { | ||
return this.models[index]; | ||
}; | ||
// Return models with matching attributes. Useful for simple cases of | ||
// `filter`. | ||
CollectionBase.prototype.where = function(attrs, first) { | ||
/** | ||
* @method | ||
* @description | ||
* Return models with matching attributes. Useful for simple cases of `filter`. | ||
* @returns {Model[]} Array of matching models. | ||
*/ | ||
CollectionBase.prototype.where = function (attrs, first) { | ||
if (_.isEmpty(attrs)) return first ? void 0 : []; | ||
return this[first ? 'find' : 'filter'](function(model) { | ||
return this[first ? 'find' : 'filter'](function (model) { | ||
for (var key in attrs) { | ||
@@ -283,12 +481,22 @@ if (attrs[key] !== model.get(key)) return false; | ||
// Return the first model with matching attributes. Useful for simple cases | ||
// of `find`. | ||
CollectionBase.prototype.findWhere = function(attrs) { | ||
/** | ||
* @method | ||
* @description | ||
* Return the first model with matching attributes. Useful for simple cases of | ||
* `find`. | ||
* @returns {Model} The first matching model. | ||
*/ | ||
CollectionBase.prototype.findWhere = function (attrs) { | ||
return this.where(attrs, true); | ||
}; | ||
// Force the collection to re-sort itself, based on a comporator defined on the model. | ||
CollectionBase.prototype.sort = function(options) { | ||
/** | ||
* @method | ||
* @private | ||
* @description | ||
* Force the collection to re-sort itself, based on a comporator defined on the model. | ||
*/ | ||
CollectionBase.prototype.sort = function (options) { | ||
if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); | ||
options || (options = {}); | ||
options = options || {}; | ||
@@ -306,69 +514,220 @@ // Run sort based on type of `comparator`. | ||
// Pluck an attribute from each model in the collection. | ||
CollectionBase.prototype.pluck = function(attr) { | ||
/** | ||
* @method | ||
* @description | ||
* Pluck an attribute from each model in the collection. | ||
* @returns {mixed[]} An array of attribute values. | ||
*/ | ||
CollectionBase.prototype.pluck = function (attr) { | ||
return this.invoke('get', attr); | ||
}; | ||
// Create a new instance of a model in this collection. Add the model to the | ||
// collection immediately, unless `wait: true` is passed, in which case we | ||
// wait for the server to agree. | ||
CollectionBase.prototype.create = function(model, options) { | ||
options = options ? _.clone(options) : {}; | ||
if (!(model = this._prepareModel(model, options))) return false; | ||
if (!options.wait) this.add(model, options); | ||
var collection = this; | ||
var success = options.success; | ||
options.success = function(model, resp, options) { | ||
if (options.wait) collection.add(model, options); | ||
if (success) success(model, resp, options); | ||
}; | ||
model.save(null, options); | ||
return model; | ||
}; | ||
// **parse** converts a response into a list of models to be added to the | ||
// collection. The default implementation is just to pass it through. | ||
CollectionBase.prototype.parse = function(resp, options) { | ||
/** | ||
* @method | ||
* @description | ||
* The `parse` method is called whenever a collection's data is returned in a | ||
* {@link CollectionBase#fetch 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. | ||
* | ||
* @param {Object[]} resp Raw database response array. | ||
*/ | ||
CollectionBase.prototype.parse = function (resp) { | ||
return resp; | ||
}; | ||
// Create a new collection with an identical list of models as this one. | ||
CollectionBase.prototype.clone = function() { | ||
return new this.constructor(this.models); | ||
/** | ||
* @method | ||
* @description | ||
* Create a new collection with an identical list of models as this one. | ||
*/ | ||
CollectionBase.prototype.clone = function () { | ||
return new this.constructor(this.models, _.pick(this, collectionProps)); | ||
}; | ||
// Private method to reset all internal state. Called when the collection | ||
// is first initialized or reset. | ||
CollectionBase.prototype._reset = function() { | ||
/** | ||
* @method | ||
* @private | ||
* @description | ||
* Reset all internal state. Called when the collection is first initialized or reset. | ||
*/ | ||
CollectionBase.prototype._reset = function () { | ||
this.length = 0; | ||
this.models = []; | ||
this._byId = Object.create(null); | ||
this._byId = Object.create(null); | ||
}; | ||
// Internal method to sever a model's ties to a collection. | ||
CollectionBase.prototype._removeReference = function(model) { | ||
// TODO: Sever from the internal model cache. | ||
}; | ||
// Internal method called every time a model in the set fires an event. | ||
// Sets need to update their indexes when models change ids. All other | ||
// events simply proxy through. "add" and "remove" events that originate | ||
// in other collections are ignored. | ||
CollectionBase.prototype._onModelEvent = function(event, model, collection, options) { | ||
// TOOD: See if we need anything here. | ||
}; | ||
// Underscore methods that we want to implement on the Collection. | ||
/** | ||
* @method CollectionBase#keys | ||
* @see http://lodash.com/docs/#keys | ||
*/ | ||
/** | ||
* @method CollectionBase#forEach | ||
* @see http://lodash.com/docs/#forEach | ||
*/ | ||
/** | ||
* @method CollectionBase#each | ||
* @see http://lodash.com/docs/#each | ||
*/ | ||
/** | ||
* @method CollectionBase#map | ||
* @see http://lodash.com/docs/#map | ||
*/ | ||
/** | ||
* @method CollectionBase#collect | ||
* @see http://lodash.com/docs/#collect | ||
*/ | ||
/** | ||
* @method CollectionBase#reduce | ||
* @see http://lodash.com/docs/#reduce | ||
*/ | ||
/** | ||
* @method CollectionBase#foldl | ||
* @see http://lodash.com/docs/#foldl | ||
*/ | ||
/** | ||
* @method CollectionBase#inject | ||
* @see http://lodash.com/docs/#inject | ||
*/ | ||
/** | ||
* @method CollectionBase#reduceRight | ||
* @see http://lodash.com/docs/#reduceRight | ||
*/ | ||
/** | ||
* @method CollectionBase#foldr | ||
* @see http://lodash.com/docs/#foldr | ||
*/ | ||
/** | ||
* @method CollectionBase#find | ||
* @see http://lodash.com/docs/#find | ||
*/ | ||
/** | ||
* @method CollectionBase#detect | ||
* @see http://lodash.com/docs/#detect | ||
*/ | ||
/** | ||
* @method CollectionBase#filter | ||
* @see http://lodash.com/docs/#filter | ||
*/ | ||
/** | ||
* @method CollectionBase#select | ||
* @see http://lodash.com/docs/#select | ||
*/ | ||
/** | ||
* @method CollectionBase#reject | ||
* @see http://lodash.com/docs/#reject | ||
*/ | ||
/** | ||
* @method CollectionBase#every | ||
* @see http://lodash.com/docs/#every | ||
*/ | ||
/** | ||
* @method CollectionBase#all | ||
* @see http://lodash.com/docs/#all | ||
*/ | ||
/** | ||
* @method CollectionBase#some | ||
* @see http://lodash.com/docs/#some | ||
*/ | ||
/** | ||
* @method CollectionBase#any | ||
* @see http://lodash.com/docs/#any | ||
*/ | ||
/** | ||
* @method CollectionBase#include | ||
* @see http://lodash.com/docs/#include | ||
*/ | ||
/** | ||
* @method CollectionBase#contains | ||
* @see http://lodash.com/docs/#contains | ||
*/ | ||
/** | ||
* @method CollectionBase#invoke | ||
* @see http://lodash.com/docs/#invoke | ||
*/ | ||
/** | ||
* @method CollectionBase#max | ||
* @see http://lodash.com/docs/#max | ||
*/ | ||
/** | ||
* @method CollectionBase#min | ||
* @see http://lodash.com/docs/#min | ||
*/ | ||
/** | ||
* @method CollectionBase#toArray | ||
* @see http://lodash.com/docs/#toArray | ||
*/ | ||
/** | ||
* @method CollectionBase#size | ||
* @see http://lodash.com/docs/#size | ||
*/ | ||
/** | ||
* @method CollectionBase#first | ||
* @see http://lodash.com/docs/#first | ||
*/ | ||
/** | ||
* @method CollectionBase#head | ||
* @see http://lodash.com/docs/#head | ||
*/ | ||
/** | ||
* @method CollectionBase#take | ||
* @see http://lodash.com/docs/#take | ||
*/ | ||
/** | ||
* @method CollectionBase#initial | ||
* @see http://lodash.com/docs/#initial | ||
*/ | ||
/** | ||
* @method CollectionBase#rest | ||
* @see http://lodash.com/docs/#rest | ||
*/ | ||
/** | ||
* @method CollectionBase#tail | ||
* @see http://lodash.com/docs/#tail | ||
*/ | ||
/** | ||
* @method CollectionBase#drop | ||
* @see http://lodash.com/docs/#drop | ||
*/ | ||
/** | ||
* @method CollectionBase#last | ||
* @see http://lodash.com/docs/#last | ||
*/ | ||
/** | ||
* @method CollectionBase#without | ||
* @see http://lodash.com/docs/#without | ||
*/ | ||
/** | ||
* @method CollectionBase#difference | ||
* @see http://lodash.com/docs/#difference | ||
*/ | ||
/** | ||
* @method CollectionBase#indexOf | ||
* @see http://lodash.com/docs/#indexOf | ||
*/ | ||
/** | ||
* @method CollectionBase#shuffle | ||
* @see http://lodash.com/docs/#shuffle | ||
*/ | ||
/** | ||
* @method CollectionBase#lastIndexOf | ||
* @see http://lodash.com/docs/#lastIndexOf | ||
*/ | ||
/** | ||
* @method CollectionBase#isEmpty | ||
* @see http://lodash.com/docs/#isEmpty | ||
*/ | ||
/** | ||
* @method CollectionBase#chain | ||
* @see http://lodash.com/docs/#chain | ||
*/ | ||
// Lodash methods that we want to implement on the Collection. | ||
// 90% of the core usefulness of Backbone Collections is actually implemented | ||
// right here: | ||
var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl', | ||
'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select', | ||
'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke', | ||
'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest', | ||
'tail', 'drop', 'last', 'without', 'difference', 'indexOf', 'shuffle', | ||
'lastIndexOf', 'isEmpty', 'chain']; | ||
var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl', 'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select', 'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke', 'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest', 'tail', 'drop', 'last', 'without', 'difference', 'indexOf', 'shuffle', 'lastIndexOf', 'isEmpty', 'chain']; | ||
// Mix in each Underscore method as a proxy to `Collection#models`. | ||
_.each(methods, function(method) { | ||
CollectionBase.prototype[method] = function() { | ||
// Mix in each Lodash method as a proxy to `Collection#models`. | ||
_.each(methods, function (method) { | ||
CollectionBase.prototype[method] = function () { | ||
var args = slice.call(arguments); | ||
@@ -380,9 +739,23 @@ args.unshift(this.models); | ||
// Underscore methods that take a property name as an argument. | ||
/** | ||
* @method CollectionBase#groupBy | ||
* @see http://lodash.com/docs/#groupBy | ||
*/ | ||
// Underscore methods that we want to implement on the Collection. | ||
/** | ||
* @method CollectionBase#countBy | ||
* @see http://lodash.com/docs/#countBy | ||
*/ | ||
// Underscore methods that we want to implement on the Collection. | ||
/** | ||
* @method CollectionBase#sortBy | ||
* @see http://lodash.com/docs/#sortBy | ||
*/ | ||
// Lodash methods that take a property name as an argument. | ||
var attributeMethods = ['groupBy', 'countBy', 'sortBy']; | ||
// Use attributes instead of properties. | ||
_.each(attributeMethods, function(method) { | ||
CollectionBase.prototype[method] = function(value, context) { | ||
var iterator = _.isFunction(value) ? value : function(model) { | ||
_.each(attributeMethods, function (method) { | ||
CollectionBase.prototype[method] = function (value, context) { | ||
var iterator = _.isFunction(value) ? value : function (model) { | ||
return model.get(value); | ||
@@ -394,8 +767,19 @@ }; | ||
// List of attributes attached directly from the `options` passed to the constructor. | ||
var modelProps = ['tableName', 'hasTimestamps']; | ||
/** | ||
* @method Collection.extend | ||
* @description | ||
* | ||
* To create a {@link Collection} class of your own, extend | ||
* `Bookshelf.Collection`. | ||
* | ||
* @param {Object=} prototypeProperties | ||
* Instance methods and properties to be attached to instances of the new | ||
* class. | ||
* @param {Object=} classProperties | ||
* Class (ie. static) functions and properties to be attached to the | ||
* constructor of the new class. | ||
* @returns {Function} Constructor for new `Collection` subclass. | ||
*/ | ||
CollectionBase.extend = require('../extend'); | ||
module.exports = CollectionBase; | ||
module.exports = CollectionBase; |
@@ -9,5 +9,7 @@ // Eager Base | ||
var _ = require('lodash'); | ||
var Promise = require('./promise'); | ||
'use strict'; | ||
var _ = require('lodash'); | ||
var Promise = require('./promise'); | ||
function EagerBase(parent, parentResponse, target) { | ||
@@ -23,8 +25,8 @@ this.parent = parent; | ||
// are necessary for fetching based on the `model.load` or `withRelated` option. | ||
fetch: Promise.method(function(options) { | ||
fetch: Promise.method(function (options) { | ||
var relationName, related, relation; | ||
var target = this.target; | ||
var handled = this.handled = {}; | ||
var target = this.target; | ||
var handled = this.handled = {}; | ||
var withRelated = this.prepWithRelated(options.withRelated); | ||
var subRelated = {}; | ||
var subRelated = {}; | ||
@@ -44,3 +46,3 @@ // Internal flag to determine whether to set the ctor(s) on the `Relation` object. | ||
var relatedObj = {}; | ||
subRelated[relationName] || (subRelated[relationName] = []); | ||
subRelated[relationName] = subRelated[relationName] || []; | ||
relatedObj[related.slice(1).join('.')] = withRelated[key]; | ||
@@ -53,3 +55,5 @@ subRelated[relationName].push(relatedObj); | ||
relation = target[relationName](); | ||
if (_.isFunction(target[relationName])) { | ||
relation = target[relationName](); | ||
} | ||
@@ -78,3 +82,3 @@ if (!relation) throw new Error(relationName + ' is not defined on the model.'); | ||
// returning the original response when these syncs & pairings are complete. | ||
return Promise.all(pendingDeferred).return(this.parentResponse); | ||
return Promise.all(pendingDeferred)['return'](this.parentResponse); | ||
}), | ||
@@ -84,3 +88,3 @@ | ||
// has a function that is called when running the query. | ||
prepWithRelated: function(withRelated) { | ||
prepWithRelated: function prepWithRelated(withRelated) { | ||
if (!_.isArray(withRelated)) withRelated = [withRelated]; | ||
@@ -90,3 +94,7 @@ var obj = {}; | ||
var related = withRelated[i]; | ||
_.isString(related) ? obj[related] = noop : _.extend(obj, related); | ||
if (_.isString(related)) { | ||
obj[related] = noop; | ||
} else { | ||
_.extend(obj, related); | ||
} | ||
} | ||
@@ -98,6 +106,6 @@ return obj; | ||
// which is used to correcly pair additional nested relations. | ||
pushModels: function(relationName, handled, resp) { | ||
var models = this.parent; | ||
pushModels: function pushModels(relationName, handled, resp) { | ||
var models = this.parent; | ||
var relatedData = handled.relatedData; | ||
var related = []; | ||
var related = []; | ||
for (var i = 0, l = resp.length; i < l; i++) { | ||
@@ -111,4 +119,4 @@ related.push(relatedData.createModel(resp[i])); | ||
var noop = function() {}; | ||
var noop = function noop() {}; | ||
module.exports = EagerBase; |
// Events | ||
// --------------- | ||
var Promise = require('./promise'); | ||
var inherits = require('inherits'); | ||
'use strict'; | ||
var Promise = require('./promise'); | ||
var inherits = require('inherits'); | ||
var EventEmitter = require('events').EventEmitter; | ||
var _ = require('lodash'); | ||
var _ = require('lodash'); | ||
@@ -16,3 +18,3 @@ function Events() { | ||
var eventSplitter = /\s+/; | ||
Events.prototype.on = function(name, handler) { | ||
Events.prototype.on = function (name, handler) { | ||
// Handle space separated event names. | ||
@@ -30,3 +32,3 @@ if (eventSplitter.test(name)) { | ||
// Add "off", "trigger", and "" method, for parity with Backbone.Events | ||
Events.prototype.off = function(event, listener) { | ||
Events.prototype.off = function (event, listener) { | ||
if (arguments.length === 0) { | ||
@@ -40,6 +42,6 @@ return this.removeAllListeners(); | ||
}; | ||
Events.prototype.trigger = function(name) { | ||
Events.prototype.trigger = function (name) { | ||
// Handle space separated event names. | ||
if (eventSplitter.test(name)) { | ||
var len = arguments.length; | ||
var len = arguments.length; | ||
var rest = new Array(len - 1); | ||
@@ -57,4 +59,7 @@ for (i = 1; i < len; i++) rest[i - 1] = arguments[i]; | ||
Events.prototype.triggerThen = function(name) { | ||
var i, l, rest, listeners = []; | ||
Events.prototype.triggerThen = function (name) { | ||
var i, | ||
l, | ||
rest, | ||
listeners = []; | ||
// Handle space separated event names. | ||
@@ -71,9 +76,13 @@ if (eventSplitter.test(name)) { | ||
switch (len) { | ||
case 1: rest = []; break; | ||
case 2: rest = [arguments[1]]; break; | ||
case 3: rest = [arguments[1], arguments[2]]; break; | ||
default: rest = new Array(len - 1); for (i = 1; i < len; i++) rest[i - 1] = arguments[i]; | ||
case 1: | ||
rest = [];break; | ||
case 2: | ||
rest = [arguments[1]];break; | ||
case 3: | ||
rest = [arguments[1], arguments[2]];break; | ||
default: | ||
rest = new Array(len - 1);for (i = 1; i < len; i++) rest[i - 1] = arguments[i]; | ||
} | ||
var events = this | ||
return Promise.try(function() { | ||
var events = this; | ||
return Promise['try'](function () { | ||
var pending = []; | ||
@@ -84,11 +93,11 @@ for (i = 0, l = listeners.length; i < l; i++) { | ||
return Promise.all(pending); | ||
}) | ||
}); | ||
}; | ||
Events.prototype.emitThen = Events.prototype.triggerThen; | ||
Events.prototype.once = function(name, callback, context) { | ||
Events.prototype.once = function (name, callback, context) { | ||
var self = this; | ||
var once = _.once(function() { | ||
self.off(name, once); | ||
return callback.apply(this, arguments); | ||
var once = _.once(function () { | ||
self.off(name, once); | ||
return callback.apply(this, arguments); | ||
}); | ||
@@ -95,0 +104,0 @@ once._callback = callback; |
// Base Model | ||
// --------------- | ||
var _ = require('lodash'); | ||
'use strict'; | ||
var _ = require('lodash'); | ||
var inherits = require('inherits'); | ||
var Events = require('./events'); | ||
var Promise = require('./promise'); | ||
var Errors = require('../errors'); | ||
var slice = Array.prototype.slice | ||
var Events = require('./events'); | ||
var slice = Array.prototype.slice; | ||
@@ -14,11 +14,16 @@ // List of attributes attached directly from the `options` passed to the constructor. | ||
// The "ModelBase" is similar to the 'Active Model' in Rails, | ||
// it defines a standard interface from which other objects may inherit. | ||
/** | ||
* @class | ||
* @classdesc | ||
* | ||
* The "ModelBase" is similar to the 'Active Model' in Rails, it defines a | ||
* standard interface from which other objects may inherit. | ||
*/ | ||
function ModelBase(attributes, options) { | ||
var attrs = attributes || {}; | ||
options = options || {}; | ||
options = options || {}; | ||
this.attributes = Object.create(null); | ||
this._reset(); | ||
this.relations = {}; | ||
this.cid = _.uniqueId('c'); | ||
this.cid = _.uniqueId('c'); | ||
if (options) { | ||
@@ -33,14 +38,62 @@ _.extend(this, _.pick(options, modelProps)); | ||
ModelBase.prototype.initialize = function() {}; | ||
ModelBase.prototype.initialize = function () {}; | ||
// The default value for the "id" attribute. | ||
/** | ||
* @name ModelBase#tableName | ||
* @member {string} | ||
* @description | ||
* | ||
* A required property for any database usage, The | ||
* {@linkcode Model#tableName tableName} property refers to the database | ||
* table name the model will query against. | ||
* | ||
* @example | ||
* | ||
* var Television = bookshelf.Model.extend({ | ||
* tableName: 'televisions' | ||
* }); | ||
*/ | ||
/** | ||
* @member {string} | ||
* @default "id" | ||
* @description | ||
* | ||
* This tells the model which attribute to expect as the unique identifier | ||
* for each database row (typically an auto-incrementing primary key named | ||
* `"id"`). Note that if you are using {@link Model#parse parse} and {@link | ||
* Model#format format} (to have your model's attributes in `camelCase`, | ||
* but your database's columns in `snake_case`, for example) this refers to | ||
* the name returned by parse (`myId`), not the database column (`my_id`). | ||
* | ||
*/ | ||
ModelBase.prototype.idAttribute = 'id'; | ||
// Get the value of an attribute. | ||
ModelBase.prototype.get = function(attr) { | ||
/** | ||
* @method | ||
* @description Get the current value of an attribute from the model. | ||
* @example note.get("title") | ||
* | ||
* @param {string} attribute - The name of the attribute to retrieve. | ||
* @returns {mixed} Attribute value. | ||
*/ | ||
ModelBase.prototype.get = function (attr) { | ||
return this.attributes[attr]; | ||
}; | ||
// Set a property. | ||
ModelBase.prototype.set = function(key, val, options) { | ||
/** | ||
* @method | ||
* @description Set a hash of attributes (one or many) on the model. | ||
* @example | ||
* | ||
* customer.set({first_name: "Joe", last_name: "Customer"}); | ||
* customer.set("telephone", "555-555-1212"); | ||
* | ||
* @param {string|Object} attribute Attribute name, or hash of attribute names and values. | ||
* @param {mixed=} value If a string was provided for `attribute`, the value to be set. | ||
* @param {Object=} options | ||
* @param {Object} [options.unset=false] Remove attributes instead of setting them. | ||
* @returns {Model} This model. | ||
*/ | ||
ModelBase.prototype.set = function (key, val, options) { | ||
if (key == null) return this; | ||
@@ -59,6 +112,5 @@ var attrs; | ||
// Extract attributes and options. | ||
var hasChanged = false; | ||
var unset = options.unset; | ||
var unset = options.unset; | ||
var current = this.attributes; | ||
var prev = this._previousAttributes; | ||
var prev = this._previousAttributes; | ||
@@ -73,7 +125,10 @@ // Check for changes of `id`. | ||
this.changed[attr] = val; | ||
if (!_.isEqual(current[attr], val)) hasChanged = true; | ||
} else { | ||
delete this.changed[attr]; | ||
} | ||
unset ? delete current[attr] : current[attr] = val; | ||
if (unset) { | ||
delete current[attr]; | ||
} else { | ||
current[attr] = val; | ||
} | ||
} | ||
@@ -83,8 +138,51 @@ return this; | ||
// A model is new if it has never been persisted, which we assume if it lacks an id. | ||
ModelBase.prototype.isNew = function() { | ||
/** | ||
* @method | ||
* @description | ||
* | ||
* Checks for the existence of an id to determine whether the model is | ||
* considered "new". | ||
* | ||
* @example | ||
* | ||
* var modelA = new bookshelf.Model(); | ||
* modelA.isNew(); // true | ||
* | ||
* var modelB = new bookshelf.Model({id: 1}); | ||
* modelB.isNew(); // false | ||
*/ | ||
ModelBase.prototype.isNew = function () { | ||
return this.id == null; | ||
}; | ||
ModelBase.prototype.serialize = function(options) { | ||
/** | ||
* @method | ||
* @description | ||
* | ||
* Return a copy of the model's {@link ModelBase#attributes attributes} for JSON | ||
* stringification. If the {@link Model model} has any relations defined, this | ||
* will also call {@link ModelBase ModelBase#toJSON} on each of the related | ||
* objects, and include them on the object unless `{shallow: true}` is | ||
* passed as an option. | ||
* | ||
* `serialize` is called internally by {@link ModelBase#toJSON toJSON}. Override | ||
* this function if you want to customize its output. | ||
* | ||
* @example | ||
* var artist = new bookshelf.Model({ | ||
* firstName: "Wassily", | ||
* lastName: "Kandinsky" | ||
* }); | ||
* | ||
* artist.set({birthday: "December 16, 1866"}); | ||
* | ||
* console.log(JSON.stringify(artist)); | ||
* // {firstName: "Wassily", lastName: "Kandinsky", birthday: "December 16, 1866"} | ||
* | ||
* @param {Object=} options | ||
* @param {bool} [options.shallow=false] Exclude relations. | ||
* @param {bool} [options.omitPivot=false] Exclude pivot values. | ||
* @returns {Object} Serialized model as a plain object. | ||
*/ | ||
ModelBase.prototype.serialize = function (options) { | ||
var attrs = _.clone(this.attributes); | ||
@@ -104,64 +202,161 @@ if (options && options.shallow) return attrs; | ||
} | ||
return attrs; | ||
} | ||
return attrs; | ||
}; | ||
// Returns an object containing a shallow copy of the model attributes, | ||
// along with the `toJSON` value of any relations, | ||
// unless `{shallow: true}` is passed in the `options`. | ||
// Also includes _pivot_ keys for relations unless `{omitPivot: true}` | ||
// is passed in `options`. | ||
ModelBase.prototype.toJSON = function(options) { | ||
return this.serialize(options) | ||
/** | ||
* @method | ||
* @description | ||
* | ||
* Called automatically by {@link | ||
* https://developer.mozilla.org/en-US/docs/Glossary/JSON#toJSON()_method | ||
* `JSON.stringify`}. To customize serialization, override {@link | ||
* BaseModel#serialize serialize}. | ||
* | ||
* @param {Object=} options Options passed to {@link BaseModel#serialize}. | ||
*/ | ||
ModelBase.prototype.toJSON = function (options) { | ||
return this.serialize(options); | ||
}; | ||
// Returns the string representation of the object. | ||
ModelBase.prototype.toString = function() { | ||
/** | ||
* @method | ||
* @private | ||
* @returns String representation of the object. | ||
*/ | ||
ModelBase.prototype.toString = function () { | ||
return '[Object Model]'; | ||
}; | ||
// Get the HTML-escaped value of an attribute. | ||
ModelBase.prototype.escape = function(key) { | ||
/** | ||
* @method | ||
* @description Get the HTML-escaped value of an attribute. | ||
* @param {string} attribute The attribute to escape. | ||
* @returns {string} HTML-escaped value of an attribute. | ||
*/ | ||
ModelBase.prototype.escape = function (key) { | ||
return _.escape(this.get(key)); | ||
}; | ||
// Returns `true` if the attribute contains a value that is not null | ||
// or undefined. | ||
ModelBase.prototype.has = function(attr) { | ||
/** | ||
* @method | ||
* @description | ||
* Returns `true` if the attribute contains a value that is not null or undefined. | ||
* @param {string} attribute The attribute to check. | ||
* @returns {bool} True if `attribute` is set, otherwise null. | ||
*/ | ||
ModelBase.prototype.has = function (attr) { | ||
return this.get(attr) != null; | ||
}; | ||
// **parse** converts a response into the hash of attributes to be `set` on | ||
// the model. The default implementation is just to pass the response along. | ||
ModelBase.prototype.parse = function(resp, options) { | ||
/** | ||
* @method | ||
* @description | ||
* | ||
* The parse method is called whenever a {@link Model model}'s data is returned | ||
* in a {@link Model#fetch fetch} call. The function is passed the raw database | ||
* response object, and should return the {@link ModelBase#attributes | ||
* attributes} hash to be {@link ModelBase#set 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 {@link | ||
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse | ||
* JSON.parse} on a text field containing JSON, or explicitly typecasting a | ||
* boolean in a sqlite3 database response. | ||
* | ||
* @example | ||
* | ||
* // Example of a "parse" to convert snake_case to camelCase, using `underscore.string` | ||
* model.parse = function(attrs) { | ||
* return _.reduce(attrs, function(memo, val, key) { | ||
* memo[_.str.camelize(key)] = val; | ||
* return memo; | ||
* }, {}); | ||
* }; | ||
* | ||
* @param {Object} response Hash of attributes to parse. | ||
* @returns {Object} Parsed attributes. | ||
*/ | ||
ModelBase.prototype.parse = function (resp) { | ||
return resp; | ||
}; | ||
// Remove an attribute from the model, firing `"change"`. `unset` is a noop | ||
// if the attribute doesn't exist. | ||
ModelBase.prototype.unset = function(attr, options) { | ||
return this.set(attr, void 0, _.extend({}, options, {unset: true})); | ||
/** | ||
* @method | ||
* @description | ||
* | ||
* Remove an attribute from the model. `unset` is a noop if the attribute | ||
* doesn't exist. | ||
* | ||
* @param attribute Attribute to unset. | ||
* @returns {Model} This model. | ||
*/ | ||
ModelBase.prototype.unset = function (attr, options) { | ||
return this.set(attr, void 0, _.extend({}, options, { unset: true })); | ||
}; | ||
// Clear all attributes on the model, firing `"change"`. | ||
ModelBase.prototype.clear = function(options) { | ||
/** | ||
* @method | ||
* @description Clear all attributes on the model. | ||
* @returns {Model} This model. | ||
*/ | ||
ModelBase.prototype.clear = function (options) { | ||
var attrs = {}; | ||
for (var key in this.attributes) attrs[key] = void 0; | ||
return this.set(attrs, _.extend({}, options, {unset: true})); | ||
return this.set(attrs, _.extend({}, options, { unset: true })); | ||
}; | ||
// **format** converts a model into the values that should be saved into | ||
// the database table. The default implementation is just to pass the data along. | ||
ModelBase.prototype.format = function(attrs, options) { | ||
/** | ||
* @method | ||
* @description | ||
* | ||
* 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 {@link Model model}, and are only used for inserting/updating - the | ||
* current values of the model are left intact. | ||
* | ||
* @param {Object} attributes The attributes to be converted. | ||
* @returns {Object} Formatted attributes. | ||
*/ | ||
ModelBase.prototype.format = function (attrs) { | ||
return attrs; | ||
}; | ||
// Returns the related item, or creates a new | ||
// related item by creating a new model or collection. | ||
ModelBase.prototype.related = function(name) { | ||
/** | ||
* @method | ||
* @description | ||
* | ||
* The `related` method returns a specified relation loaded on the relations | ||
* hash on the model, or calls the associated relation method and adds it to | ||
* the relations hash if one exists and has not yet been loaded. | ||
* | ||
* @example | ||
* | ||
* new Photo({id: 1}).fetch({ | ||
* withRelated: ['account'] | ||
* }).then(function(photo) { | ||
* if (photo) { | ||
* var account = photo.related('account'); | ||
* if (account.id) { | ||
* return account.related('trips').fetch(); | ||
* } | ||
* } | ||
* }); | ||
* | ||
* @returns {Model|Collection|undefined} The specified relation as defined by a | ||
* method on the model, or undefined if it does not exist. | ||
*/ | ||
ModelBase.prototype.related = function (name) { | ||
return this.relations[name] || (this[name] ? this.relations[name] = this[name]() : void 0); | ||
}; | ||
// Create a new model with identical attributes to this one, | ||
// including any relations on the current model. | ||
ModelBase.prototype.clone = function(options) { | ||
/** | ||
* @method | ||
* @description | ||
* Returns a new instance of the model with identical {@link | ||
* ModelBase#attributes attributes}, including any relations from the cloned | ||
* model. | ||
* | ||
* @returns {Model} Cloned instance of this model. | ||
*/ | ||
ModelBase.prototype.clone = function () { | ||
var model = new this.constructor(this.attributes); | ||
@@ -173,19 +368,79 @@ var relations = this.relations; | ||
model._previousAttributes = _.clone(this._previousAttributes); | ||
model.changed = setProps(Object.create(null), this.changed); | ||
model.changed = _.clone(this.changed); | ||
return model; | ||
}; | ||
// Sets the timestamps before saving the model. | ||
ModelBase.prototype.timestamp = function(options) { | ||
var d = new Date(); | ||
var keys = (_.isArray(this.hasTimestamps) ? this.hasTimestamps : ['created_at', 'updated_at']); | ||
var vals = {}; | ||
if (keys[1]) vals[keys[1]] = d; | ||
if (this.isNew(options) && keys[0] && (!options || options.method !== 'update')) vals[keys[0]] = d; | ||
return vals; | ||
/** | ||
* @method | ||
* @private | ||
* @description | ||
* | ||
* Returns the method that will be used on save, either 'update' or 'insert'. | ||
* This is an internal helper that uses `isNew` and `options.method` to | ||
* determine the correct method. If `option.method` is provided, it will be | ||
* returned, but lowercased for later comparison. | ||
* | ||
* @returns {string} Either `'insert'` or `'update'`. | ||
*/ | ||
ModelBase.prototype.saveMethod = function (options) { | ||
var method = options && options.method && options.method.toLowerCase(); | ||
return method || (this.isNew(options) ? 'insert' : 'update'); | ||
}; | ||
// Determine if the model has changed since the last `"change"` event. | ||
// If you specify an attribute name, determine if that attribute has changed. | ||
ModelBase.prototype.hasChanged = function(attr) { | ||
/** | ||
* @method | ||
* @description | ||
* Sets the timestamp attributes on the model, if {@link Model#hasTimestamps | ||
* hasTimestamps} is set to `true` or an array. Check if the model {@link | ||
* Model#isNew isNew} or if `{method: 'insert'}` is provided as an option and | ||
* set the `created_at` and `updated_at` attributes to the current date if it | ||
* is being inserted, and just the `updated_at` attribute if it's being updated. | ||
* This method may be overriden to use different column names or types for the | ||
* timestamps. | ||
* | ||
* @param {Object=} options | ||
* @param {string} [options.method="update"] | ||
* Either `'insert'` or `'update'`. Specify what kind of save the attribute | ||
* update is for. | ||
* | ||
* @returns {Object} A hash of timestamp attributes that were set. | ||
*/ | ||
ModelBase.prototype.timestamp = function (options) { | ||
if (!this.hasTimestamps) return {}; | ||
var now = new Date(); | ||
var attributes = {}; | ||
var method = this.saveMethod(options); | ||
var keys = _.isArray(this.hasTimestamps) ? this.hasTimestamps : ['created_at', 'updated_at']; | ||
var createdAtKey = keys[0]; | ||
var updatedAtKey = keys[1]; | ||
if (updatedAtKey) { | ||
attributes[updatedAtKey] = now; | ||
} | ||
if (createdAtKey && method === 'insert') { | ||
attributes[createdAtKey] = now; | ||
} | ||
this.set(attributes, options); | ||
return attributes; | ||
}; | ||
/** | ||
* @method | ||
* @description | ||
* | ||
* Returns true if any {@link Model#attributes attribute} attribute has changed | ||
* since the last {@link Model#fetch fetch}, {@link Model#save save}, or {@link | ||
* Model#destroy destroy}. If an attribute is passed, returns true only if that | ||
* specific attribute has changed. | ||
* | ||
* @param {string=} attribute | ||
* @returns {bool} | ||
* `true` if any attribute has changed. Or, if `attribute` was specified, true | ||
* if it has changed. | ||
*/ | ||
ModelBase.prototype.hasChanged = function (attr) { | ||
if (attr == null) return !_.isEmpty(this.changed); | ||
@@ -195,5 +450,13 @@ return _.has(this.changed, attr); | ||
// Get the previous value of an attribute, recorded at the time the last | ||
// `"change"` event was fired. | ||
ModelBase.prototype.previous = function(attr) { | ||
/** | ||
* @method | ||
* @description | ||
* | ||
* Returns the this previous value of a changed {@link Model#attributes | ||
* attribute}, or `undefined` if one had not been specified previously. | ||
* | ||
* @param {string} attribute The attribute to check | ||
* @returns {mixed} The previous value | ||
*/ | ||
ModelBase.prototype.previous = function (attr) { | ||
if (attr == null || !this._previousAttributes) return null; | ||
@@ -203,11 +466,28 @@ return this._previousAttributes[attr]; | ||
// Get all of the attributes of the model at the time of the previous | ||
// `"change"` event. | ||
ModelBase.prototype.previousAttributes = function() { | ||
/** | ||
* @method | ||
* @description | ||
* | ||
* Return a copy of the {@link Model model}'s previous attributes from the | ||
* model's last {@link Model#fetch fetch}, {@link Model#save save}, or {@link | ||
* Model#destroy destroy}. Useful for getting a diff between versions of a | ||
* model, or getting back to a valid state after an error occurs. | ||
* | ||
* @returns {Object} The attributes as they were before the last change. | ||
*/ | ||
ModelBase.prototype.previousAttributes = function () { | ||
return _.clone(this._previousAttributes); | ||
}; | ||
// Resets the `_previousAttributes` and `changed` hash for the model. | ||
// Typically called after a `sync` action (save, fetch, delete) - | ||
ModelBase.prototype._reset = function() { | ||
/** | ||
* @method | ||
* @private | ||
* @description | ||
* | ||
* Resets the `_previousAttributes` and `changed` hash for the model. | ||
* Typically called after a `sync` action (save, fetch, delete) - | ||
* | ||
* @returns {Model} This model. | ||
*/ | ||
ModelBase.prototype._reset = function () { | ||
this._previousAttributes = _.clone(this.attributes); | ||
@@ -218,11 +498,26 @@ this.changed = Object.create(null); | ||
// Set the changed properties on the object. | ||
function setProps(obj, hash) { | ||
var i = -1, keys = Object.keys(hash); | ||
while (++i < hash.length) { | ||
var key = hash[i] | ||
obj[key] = hash[key]; | ||
} | ||
} | ||
/** | ||
* @method ModelBase#keys | ||
* @see http://lodash.com/docs/#keys | ||
*/ | ||
/** | ||
* @method ModelBase#values | ||
* @see http://lodash.com/docs/#values | ||
*/ | ||
/** | ||
* @method ModelBase#pairs | ||
* @see http://lodash.com/docs/#pairs | ||
*/ | ||
/** | ||
* @method ModelBase#invert | ||
* @see http://lodash.com/docs/#invert | ||
*/ | ||
/** | ||
* @method ModelBase#pick | ||
* @see http://lodash.com/docs/#pick | ||
*/ | ||
/** | ||
* @method ModelBase#omit | ||
* @see http://lodash.com/docs/#omit | ||
*/ | ||
// "_" methods that we want to implement on the Model. | ||
@@ -232,4 +527,4 @@ var modelMethods = ['keys', 'values', 'pairs', 'invert', 'pick', 'omit']; | ||
// Mix in each "_" method as a proxy to `Model#attributes`. | ||
_.each(modelMethods, function(method) { | ||
ModelBase.prototype[method] = function() { | ||
_.each(modelMethods, function (method) { | ||
ModelBase.prototype[method] = function () { | ||
var args = slice.call(arguments); | ||
@@ -241,4 +536,73 @@ args.unshift(this.attributes); | ||
/** | ||
* @method Model.extend | ||
* @description | ||
* | ||
* To create a Model class of your own, you extend {@link Model bookshelf.Model}. | ||
* | ||
* `extend` correctly sets up the prototype chain, so subclasses created with | ||
* `extend` can be further extended and subclassed as far as you like. | ||
* | ||
* var checkit = require('checkit'); | ||
* var Promise = require('bluebird'); | ||
* var bcrypt = Promise.promisifyAll(require('bcrypt')); | ||
* | ||
* var Customer = bookshelf.Model.extend({ | ||
* | ||
* initialize: function() { | ||
* this.on('saving', this.validateSave); | ||
* }, | ||
* | ||
* validateSave: function() { | ||
* return checkit(rules).run(this.attributes); | ||
* }, | ||
* | ||
* account: function() { | ||
* return this.belongsTo(Account); | ||
* }, | ||
* | ||
* }, { | ||
* | ||
* login: Promise.method(function(email, password) { | ||
* if (!email || !password) throw new Error('Email and password are both required'); | ||
* return new this({email: email.toLowerCase().trim()}).fetch({require: true}).tap(function(customer) { | ||
* return bcrypt.compareAsync(customer.get('password'), password); | ||
* }); | ||
* }) | ||
* | ||
* }); | ||
* | ||
* Customer.login(email, password) | ||
* .then(function(customer) { | ||
* res.json(customer.omit('password')); | ||
* }).catch(Customer.NotFoundError, function() { | ||
* res.json(400, {error: email + ' not found'}); | ||
* }).catch(function(err) { | ||
* console.error(err); | ||
* }); | ||
* | ||
* _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 {@link Model#set set}, or {@link | ||
* Model#save 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); | ||
* ... | ||
* } | ||
* }); | ||
* | ||
* @param {Object=} prototypeProperties | ||
* Instance methods and properties to be attached to instances of the new | ||
* class. | ||
* @param {Object=} classProperties | ||
* Class (ie. static) functions and properties to be attached to the | ||
* constructor of the new class. | ||
* @returns {Function} Constructor for new `Model` subclass. | ||
*/ | ||
ModelBase.extend = require('../extend'); | ||
module.exports = ModelBase; | ||
module.exports = ModelBase; |
@@ -0,18 +1,20 @@ | ||
'use strict'; | ||
var Promise = require('bluebird/js/main/promise')(); | ||
var helpers = require('../helpers') | ||
var helpers = require('../helpers'); | ||
Promise.prototype.yield = function() { | ||
helpers.deprecate('.yield', '.return') | ||
return this.return.apply(this, arguments); | ||
} | ||
Promise.prototype.ensure = function() { | ||
helpers.deprecate('.ensure', '.finally') | ||
return this.finally.apply(this, arguments); | ||
} | ||
Promise.prototype.otherwise = function() { | ||
helpers.deprecate('.otherwise', '.catch') | ||
return this.catch.apply(this, arguments); | ||
} | ||
Promise.prototype.exec = function() { | ||
helpers.deprecate('bookshelf.exec', 'bookshelf.asCallback') | ||
Promise.prototype['yield'] = function () { | ||
helpers.deprecate('.yield', '.return'); | ||
return this['return'].apply(this, arguments); | ||
}; | ||
Promise.prototype.ensure = function () { | ||
helpers.deprecate('.ensure', '.finally'); | ||
return this['finally'].apply(this, arguments); | ||
}; | ||
Promise.prototype.otherwise = function () { | ||
helpers.deprecate('.otherwise', '.catch'); | ||
return this['catch'].apply(this, arguments); | ||
}; | ||
Promise.prototype.exec = function () { | ||
helpers.deprecate('bookshelf.exec', 'bookshelf.asCallback'); | ||
return this.nodeify.apply(this, arguments); | ||
@@ -19,0 +21,0 @@ }; |
// Base Relation | ||
// --------------- | ||
var _ = require('lodash'); | ||
'use strict'; | ||
var _ = require('lodash'); | ||
var CollectionBase = require('./collection'); | ||
@@ -11,4 +13,5 @@ | ||
this.type = type; | ||
if (this.target = Target) { | ||
this.targetTableName = _.result(Target.prototype, 'tableName'); | ||
this.target = Target; | ||
if (this.target) { | ||
this.targetTableName = _.result(Target.prototype, 'tableName'); | ||
this.targetIdAttribute = _.result(Target.prototype, 'idAttribute'); | ||
@@ -23,3 +26,3 @@ } | ||
// dealing with `morphTo` cases, where the same relation is targeting multiple models. | ||
instance: function(type, Target, options) { | ||
instance: function instance(type, Target, options) { | ||
return new this.constructor(type, Target, options); | ||
@@ -30,3 +33,3 @@ }, | ||
// methods. (Parsing may mutate information necessary for eager pairing.) | ||
createModel: function(data) { | ||
createModel: function createModel(data) { | ||
if (this.target.prototype instanceof CollectionBase) { | ||
@@ -39,3 +42,3 @@ return new this.target.prototype.model(data)._reset(); | ||
// Eager pair the models. | ||
eagerPair: function() {} | ||
eagerPair: function eagerPair() {} | ||
@@ -42,0 +45,0 @@ }); |
@@ -1,61 +0,198 @@ | ||
// Collection | ||
// --------------- | ||
var _ = require('lodash'); | ||
'use strict'; | ||
var Sync = require('./sync'); | ||
var Helpers = require('./helpers'); | ||
var EagerRelation = require('./eager'); | ||
var Errors = require('./errors'); | ||
exports.__esModule = true; | ||
var CollectionBase = require('./base/collection'); | ||
var Promise = require('./base/promise'); | ||
var createError = require('create-error'); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } | ||
var BookshelfCollection = CollectionBase.extend({ | ||
var _lodash = require('lodash'); | ||
// Used to define passthrough relationships - `hasOne`, `hasMany`, | ||
// `belongsTo` or `belongsToMany`, "through" a `Interim` model or collection. | ||
through: function(Interim, foreignKey, otherKey) { | ||
return this.relatedData.through(this, Interim, {throughForeignKey: foreignKey, otherKey: otherKey}); | ||
var _lodash2 = _interopRequireDefault(_lodash); | ||
var _sync = require('./sync'); | ||
var _sync2 = _interopRequireDefault(_sync); | ||
var _helpers = require('./helpers'); | ||
var _helpers2 = _interopRequireDefault(_helpers); | ||
var _eager = require('./eager'); | ||
var _eager2 = _interopRequireDefault(_eager); | ||
var _errors = require('./errors'); | ||
var _errors2 = _interopRequireDefault(_errors); | ||
var _baseCollection = require('./base/collection'); | ||
var _baseCollection2 = _interopRequireDefault(_baseCollection); | ||
var _basePromise = require('./base/promise'); | ||
var _basePromise2 = _interopRequireDefault(_basePromise); | ||
var _createError = require('create-error'); | ||
var _createError2 = _interopRequireDefault(_createError); | ||
/** | ||
* @class Collection | ||
* @extends CollectionBase | ||
* @inheritdoc | ||
* @classdesc | ||
* | ||
* Collections are ordered sets of models returned from the database, from a | ||
* {@link Model#fetchAll fetchAll} call. They may be used with a suite of | ||
* {@link http://lodash.com/ Lodash} methods. | ||
* | ||
* @constructor | ||
* @description | ||
* | ||
* When creating a {@link Collection}, you may choose to pass in the initial | ||
* array of {@link Model models}. The collection's {@link Collection#comparator | ||
* comparator} may be included as an option. Passing `false` as the comparator | ||
* option will prevent sorting. If you define an {@link Collection#initialize | ||
* initialize} function, it will be invoked when the collection is created. | ||
* | ||
* @example | ||
* let tabs = new TabSet([tab1, tab2, tab3]); | ||
* | ||
* @param {(Model[])=} models Initial array of models. | ||
* @param {Object=} options | ||
* @param {bool} [options.comparator=false] | ||
* {@link Collection#comparator Comparator} for collection, or `false` to disable sorting. | ||
*/ | ||
var BookshelfCollection = _baseCollection2['default'].extend({ | ||
/** | ||
* @method Collection#through | ||
* @private | ||
* @description | ||
* Used to define passthrough relationships - `hasOne`, `hasMany`, `belongsTo` | ||
* or `belongsToMany`, "through" an `Interim` model or collection. | ||
*/ | ||
through: function through(Interim, foreignKey, otherKey) { | ||
return this.relatedData.through(this, Interim, { throughForeignKey: foreignKey, otherKey: otherKey }); | ||
}, | ||
// Fetch the models for this collection, resetting the models | ||
// for the query when they arrive. | ||
fetch: Promise.method(function(options) { | ||
options = options ? _.clone(options) : {}; | ||
return this.sync(options) | ||
.select() | ||
.bind(this) | ||
.tap(function(response) { | ||
if (!response || response.length === 0) { | ||
if (options.require) throw new this.constructor.EmptyError('EmptyResponse'); | ||
return Promise.reject(null); | ||
} | ||
}) | ||
/** | ||
* @method Collection#fetch | ||
* @description | ||
* 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 {@link Collection#fetch fetch} call. A {@link | ||
* Collection#fetched "fetched"} event will be fired when records are | ||
* successfully retrieved. If you need to constrain the query performed by | ||
* `fetch`, you can call the {@link Collection#query query} method before | ||
* calling `fetch`. | ||
* | ||
* *If you'd like to only fetch specific columns, you may specify a `columns` | ||
* property in the options for the `fetch` call.* | ||
* | ||
* The `withRelated` option may be specified to fetch the models of the | ||
* collection, eager loading any specified {@link Relation 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 respective models, | ||
* may be retrieved with the {@link Model#related related} method. | ||
* | ||
* @fires Collection#fetched | ||
* @throws {Collection.EmptyError} | ||
* Upon a sucessful query resulting in no records returns. Only fired if `require: true` is passed as an option. | ||
* | ||
* @param {Object=} options | ||
* @param {bool} [options.required=false] Trigger a {@link Collection.EmptyError} if no records are found. | ||
* @param {string|string[]} [options.withRelated=[]] A relation, or list of relations, to be eager loaded as part of the `fetch` operation. | ||
* @returns {Promise<Collection>} | ||
*/ | ||
fetch: _basePromise2['default'].method(function (options) { | ||
options = options ? _lodash2['default'].clone(options) : {}; | ||
return this.sync(options).select().bind(this).tap(function (response) { | ||
if (!response || response.length === 0) { | ||
if (options.require) throw new this.constructor.EmptyError('EmptyResponse'); | ||
return _basePromise2['default'].reject(null); | ||
} | ||
}) | ||
// Now, load all of the data onto the collection as necessary. | ||
.tap(this._handleResponse) | ||
// Now, load all of the data onto the collection as necessary. | ||
.tap(this._handleResponse) | ||
// If the "withRelated" is specified, we also need to eager load all of the | ||
// data on the collection, as a side-effect, before we ultimately jump into the | ||
// next step of the collection. Since the `columns` are only relevant to the current | ||
// level, ensure those are omitted from the options. | ||
.tap(function(response) { | ||
if (options.withRelated) { | ||
return this._handleEager(response, _.omit(options, 'columns')); | ||
} | ||
}) | ||
.tap(function(response) { | ||
return this.triggerThen('fetched', this, response, options); | ||
}) | ||
.catch(function(err) { | ||
if (err !== null) throw err; | ||
this.reset([], {silent: true}); | ||
}) | ||
.return(this); | ||
// If the "withRelated" is specified, we also need to eager load all of the | ||
// data on the collection, as a side-effect, before we ultimately jump into the | ||
// next step of the collection. Since the `columns` are only relevant to the current | ||
// level, ensure those are omitted from the options. | ||
.tap(function (response) { | ||
if (options.withRelated) { | ||
return this._handleEager(response, _lodash2['default'].omit(options, 'columns')); | ||
} | ||
}).tap(function (response) { | ||
/** | ||
* @event Collection#fetched | ||
* | ||
* @description | ||
* Fired after a `fetch` operation. A promise may be returned from the | ||
* event handler for async behaviour. | ||
* | ||
* @param {Collection} collection The collection performing the {@link Collection#fetch}. | ||
* @param {Object} reponse Knex query response. | ||
* @param {Object} options Options object passed to {@link Collection#fetch fetch}. | ||
* @returns {Promise} | ||
*/ | ||
return this.triggerThen('fetched', this, response, options); | ||
})['catch'](function (err) { | ||
if (err !== null) throw err; | ||
this.reset([], { silent: true }); | ||
})['return'](this); | ||
}), | ||
// Fetches a single model from the collection, useful on related collections. | ||
fetchOne: Promise.method(function(options) { | ||
var model = new this.model; | ||
// Counts all models in collection, respecting relational constrains and query | ||
// modifications. | ||
count: _basePromise2['default'].method(function (column, options) { | ||
if (!_lodash2['default'].isString(column)) { | ||
options = column; | ||
column = undefined; | ||
} | ||
if (options) options = _lodash2['default'].clone(options); | ||
return this.sync(options).count(column); | ||
}), | ||
/** | ||
* @method Collection#fetchOne | ||
* @description | ||
* | ||
* Fetch and return a single {@link Model model} from the collection, | ||
* maintaining any {@link Relation relation} data from the collection, and | ||
* any {@link Collection#query query} parameters that have already been passed | ||
* to the collection. Especially helpful on relations, where you would only | ||
* like to return a single model from the associated collection. | ||
* | ||
* @example | ||
* | ||
* // select * from authors where site_id = 1 and id = 2 limit 1; | ||
* new Site({id:1}) | ||
* .authors() | ||
* .query({where: {id: 2}}) | ||
* .fetchOne() | ||
* .then(function(model) { | ||
* // ... | ||
* }); | ||
* | ||
* @param {Object=} options | ||
* @param {boolean} [options.require=false] | ||
* If `true`, will reject the returned response with a {@link | ||
* Model.NotFoundError NotFoundError} if no result is found. | ||
* @param {(string|string[])} [options.columns='*'] | ||
* Limit the number of columns fetched. | ||
* @param {Transaction} options.transacting | ||
* Optionally run the query in a transaction. | ||
* | ||
* @throws {Model.NotFoundError} | ||
* @returns {Promise<Model|undefined>} | ||
* A promise resolving to the fetched {@link Model model} or `undefined` if none exists. | ||
*/ | ||
fetchOne: _basePromise2['default'].method(function (options) { | ||
var model = new this.model(); | ||
model._knex = this.query().clone(); | ||
@@ -67,18 +204,42 @@ this.resetQuery(); | ||
// Eager loads relationships onto an already populated `Collection` instance. | ||
load: Promise.method(function(relations, options) { | ||
_.isArray(relations) || (relations = [relations]); | ||
options = _.extend({}, options, {shallow: true, withRelated: relations}); | ||
return new EagerRelation(this.models, this.toJSON(options), new this.model()) | ||
.fetch(options) | ||
.return(this); | ||
/** | ||
* @method Collection#load | ||
* @description | ||
* `load` is used to eager load relations onto a Collection, in a similar way | ||
* that the `withRelated` property works on {@link Collection#fetch fetch}. | ||
* Nested eager loads can be specified by separating the nested relations with | ||
* `'.'`. | ||
* | ||
* @param {string|string[]} relations The relation, or relations, to be loaded. | ||
* @param {Object=} options Hash of options. | ||
* @param {Transaction=} options.transacting | ||
* | ||
* @returns {Promise<Collection>} A promise resolving to this {@link | ||
* Collection collection} | ||
*/ | ||
load: _basePromise2['default'].method(function (relations, options) { | ||
if (!_lodash2['default'].isArray(relations)) relations = [relations]; | ||
options = _lodash2['default'].extend({}, options, { shallow: true, withRelated: relations }); | ||
return new _eager2['default'](this.models, this.toJSON(options), new this.model()).fetch(options)['return'](this); | ||
}), | ||
// Shortcut for creating a new model, saving, and adding to the collection. | ||
// Returns a promise which will resolve with the model added to the collection. | ||
// If the model is a relation, put the `foreignKey` and `fkValue` from the `relatedData` | ||
// hash into the inserted model. Also, if the model is a `manyToMany` relation, | ||
// automatically create the joining model upon insertion. | ||
create: Promise.method(function(model, options) { | ||
options = options ? _.clone(options) : {}; | ||
/** | ||
* @method Collection#create | ||
* @description | ||
* | ||
* Convenience to create a new instance of a {@link Model model} within a | ||
* collection. Equivalent to instantiating a model with a hash of {@link | ||
* Model#attributes attributes}, {@link Model#save saving} the model to the | ||
* database, and adding the model to the collection after being successfully | ||
* created. | ||
* | ||
* @param {Object} model A set of attributes to be set on the new model. | ||
* @param {Object=} options | ||
* @param {Transaction=} options.transacting | ||
* | ||
* @returns {Promise<Model>} A promise resolving with the new {@link Modle | ||
* model}. | ||
*/ | ||
create: _basePromise2['default'].method(function (model, options) { | ||
options = options ? _lodash2['default'].clone(options) : {}; | ||
var relatedData = this.relatedData; | ||
@@ -93,18 +254,21 @@ model = this._prepareModel(model, options); | ||
} | ||
return Helpers | ||
.saveConstraints(model, relatedData) | ||
.save(null, options) | ||
.bind(this) | ||
.then(function() { | ||
if (relatedData && (relatedData.type === 'belongsToMany' || relatedData.isThrough())) { | ||
return this.attach(model, _.omit(options, 'query')); | ||
} | ||
}) | ||
.then(function() { this.add(model, options); }) | ||
.return(model); | ||
return _helpers2['default'].saveConstraints(model, relatedData).save(null, options).bind(this).then(function () { | ||
if (relatedData && relatedData.type === 'belongsToMany') { | ||
return this.attach(model, _lodash2['default'].omit(options, 'query')); | ||
} | ||
}).then(function () { | ||
this.add(model, options); | ||
})['return'](model); | ||
}), | ||
// Reset the query builder, called internally | ||
// each time a query is run. | ||
resetQuery: function() { | ||
/** | ||
* @method Collection#resetQuery | ||
* @description | ||
* 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 {@link Sync}. | ||
* | ||
* @returns {Collection} Self, this method is chainable. | ||
*/ | ||
resetQuery: function resetQuery() { | ||
this._knex = null; | ||
@@ -114,16 +278,59 @@ return this; | ||
// Returns an instance of the query builder. | ||
query: function() { | ||
return Helpers.query(this, _.toArray(arguments)); | ||
/** | ||
* @method Collection#query | ||
* @description | ||
* | ||
* `query` 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. If the `method` argument is a function, it will be | ||
* called with the Knex query builder as the context and the first argument. | ||
* | ||
* @example | ||
* | ||
* let qb = collection.query(); | ||
* qb.where({id: 1}).select().then(function(resp) {... | ||
* | ||
* collection.query(function(qb) { | ||
* qb.where('id', '>', 5).andWhere('first_name', '=', 'Test'); | ||
* }).fetch() | ||
* .then(function(collection) {... | ||
* | ||
* collection | ||
* .query('where', 'other_id', '=', '5') | ||
* .fetch() | ||
* .then(function(collection) { | ||
* ... | ||
* }); | ||
* | ||
* @param {function|Object|...string=} arguments The query method. | ||
* @returns {Collection|QueryBuilder} | ||
* Will return this model or, if called with no arguments, the underlying query builder. | ||
* | ||
* @see {@link http://knexjs.org/#Builder Knex `QueryBuilder`} | ||
*/ | ||
query: function query() { | ||
return _helpers2['default'].query(this, _lodash2['default'].toArray(arguments)); | ||
}, | ||
// Creates and returns a new `Bookshelf.Sync` instance. | ||
sync: function(options) { | ||
return new Sync(this, options); | ||
/** | ||
* @method Collection#query | ||
* @private | ||
* @description Creates and returns a new `Bookshelf.Sync` instance. | ||
*/ | ||
sync: function sync(options) { | ||
return new _sync2['default'](this, options); | ||
}, | ||
// Handles the response data for the collection, returning from the collection's fetch call. | ||
_handleResponse: function(response) { | ||
/** | ||
* @method Collection#_handleResponse | ||
* @private | ||
* @description | ||
* Handles the response data for the collection, returning from the | ||
* collection's `fetch` call. | ||
*/ | ||
_handleResponse: function _handleResponse(response) { | ||
var relatedData = this.relatedData; | ||
this.set(response, {silent: true, parse: true}).invoke('_reset'); | ||
this.set(response, { silent: true, parse: true }).invoke('_reset'); | ||
if (relatedData && relatedData.isJoined()) { | ||
@@ -134,5 +341,10 @@ relatedData.parsePivot(this.models); | ||
// Handle the related data loading on the collection. | ||
_handleEager: function(response, options) { | ||
return new EagerRelation(this.models, response, new this.model()).fetch(options); | ||
/** | ||
* @method Collection#_handleEager | ||
* @private | ||
* @description | ||
* Handle the related data loading on the collection. | ||
*/ | ||
_handleEager: function _handleEager(response, options) { | ||
return new _eager2['default'](this.models, response, new this.model()).fetch(options); | ||
} | ||
@@ -142,4 +354,10 @@ | ||
extended: function(child) { | ||
child.EmptyError = createError(this.EmptyError) | ||
extended: function extended(child) { | ||
/** | ||
* @class Collection.NotFoundError | ||
* @description | ||
* Thrown when no records are found by {@link Collection#fetch fetch}, | ||
* when called with the `{require: true}` option. | ||
*/ | ||
child.EmptyError = (0, _createError2['default'])(this.EmptyError); | ||
} | ||
@@ -149,4 +367,5 @@ | ||
BookshelfCollection.EmptyError = Errors.EmptyError | ||
BookshelfCollection.EmptyError = _errors2['default'].EmptyError; | ||
module.exports = BookshelfCollection; | ||
exports['default'] = BookshelfCollection; | ||
module.exports = exports['default']; |
// EagerRelation | ||
// --------------- | ||
var _ = require('lodash'); | ||
var inherits = require('inherits'); | ||
'use strict'; | ||
var Helpers = require('./helpers'); | ||
var Promise = require('./base/promise'); | ||
var _ = require('lodash'); | ||
var inherits = require('inherits'); | ||
var Helpers = require('./helpers'); | ||
var Promise = require('./base/promise'); | ||
var EagerBase = require('./base/eager'); | ||
@@ -23,3 +25,3 @@ | ||
// and any options needed for the current fetch. | ||
eagerFetch: Promise.method(function(relationName, handled, options) { | ||
eagerFetch: Promise.method(function (relationName, handled, options) { | ||
var relatedData = handled.relatedData; | ||
@@ -32,9 +34,5 @@ | ||
return handled | ||
.sync(_.extend(options, {parentResponse: this.parentResponse})) | ||
.select() | ||
.bind(this) | ||
.tap(function(response) { | ||
return this._eagerLoadHelper(response, relationName, handled, _.omit(options, 'parentResponse')); | ||
}); | ||
return handled.sync(_.extend(options, { parentResponse: this.parentResponse })).select().bind(this).tap(function (response) { | ||
return this._eagerLoadHelper(response, relationName, handled, _.omit(options, 'parentResponse')); | ||
}); | ||
}), | ||
@@ -45,27 +43,21 @@ | ||
// pairing them up onto a single response for the eager loading. | ||
morphToFetch: Promise.method(function(relationName, relatedData, options) { | ||
var groups = _.groupBy(this.parent, function(m) { | ||
morphToFetch: Promise.method(function (relationName, relatedData, options) { | ||
var _this = this; | ||
var groups = _.groupBy(this.parent, function (m) { | ||
var typeKeyName = relatedData.columnNames && relatedData.columnNames[0] ? relatedData.columnNames[0] : relatedData.morphName + '_type'; | ||
return m.get(typeKeyName); | ||
}); | ||
var pending = _.reduce(groups, function(memo, val, group) { | ||
var pending = _.reduce(groups, function (memo, val, group) { | ||
var Target = Helpers.morphCandidate(relatedData.candidates, group); | ||
var target = new Target(); | ||
var idKeyName = relatedData.columnNames && relatedData.columnNames[1] ? relatedData.columnNames[1] : relatedData.morphName + '_id'; | ||
memo.push(target | ||
.query('whereIn', | ||
_.result(target, 'idAttribute'), | ||
_.uniq(_.invoke(groups[group], 'get', idKeyName)) | ||
) | ||
.sync(options) | ||
.select() | ||
.bind(this) | ||
.tap(function(response) { | ||
return this._eagerLoadHelper(response, relationName, { | ||
relatedData: relatedData.instance('morphTo', Target, {morphName: relatedData.morphName, columnNames: relatedData.columnNames}) | ||
}, options); | ||
})); | ||
return memo; | ||
}, [], this); | ||
return Promise.all(pending).then(function(resps) { | ||
memo.push(target.query('whereIn', _.result(target, 'idAttribute'), _.uniq(_.invoke(groups[group], 'get', idKeyName))).sync(options).select().bind(_this).tap(function (response) { | ||
return this._eagerLoadHelper(response, relationName, { | ||
relatedData: relatedData.instance('morphTo', Target, { morphName: relatedData.morphName, columnNames: relatedData.columnNames }) | ||
}, options); | ||
})); | ||
return memo; | ||
}, []); | ||
return Promise.all(pending).then(function (resps) { | ||
return _.flatten(resps); | ||
@@ -76,5 +68,5 @@ }); | ||
// Handles the eager load for both the `morphTo` and regular cases. | ||
_eagerLoadHelper: function(response, relationName, handled, options) { | ||
_eagerLoadHelper: function _eagerLoadHelper(response, relationName, handled, options) { | ||
var relatedModels = this.pushModels(relationName, handled, response); | ||
var relatedData = handled.relatedData; | ||
var relatedData = handled.relatedData; | ||
@@ -90,5 +82,5 @@ // If there is a response, fetch additional nested eager relations, if any. | ||
if (withRelated.length === 0) return; | ||
options = _.extend({}, options, {withRelated: withRelated}); | ||
options = _.extend({}, options, { withRelated: withRelated }); | ||
} | ||
return new EagerRelation(relatedModels, response, relatedModel).fetch(options).return(response); | ||
return new EagerRelation(relatedModels, response, relatedModel).fetch(options)['return'](response); | ||
} | ||
@@ -99,7 +91,7 @@ }, | ||
// relations are attempted for loading. | ||
_filterRelated: function(relatedModel, options) { | ||
_filterRelated: function _filterRelated(relatedModel, options) { | ||
// By this point, all withRelated should be turned into a hash, so it should | ||
// be fairly simple to process by splitting on the dots. | ||
return _.reduce(options.withRelated, function(memo, val) { | ||
return _.reduce(options.withRelated, function (memo, val) { | ||
for (var key in val) { | ||
@@ -115,2 +107,2 @@ var seg = key.split('.')[0]; | ||
module.exports = EagerRelation; | ||
module.exports = EagerRelation; |
@@ -0,1 +1,3 @@ | ||
'use strict'; | ||
var createError = require('create-error'); | ||
@@ -18,2 +20,2 @@ | ||
}; | ||
}; |
// Uses a hash of prototype properties and class properties to be extended. | ||
module.exports = function(protoProps, staticProps) { | ||
"use strict"; | ||
module.exports = function (protoProps, staticProps) { | ||
var parent = this; | ||
@@ -10,25 +12,28 @@ var child; | ||
// by us to simply call the parent's constructor. | ||
if (protoProps && protoProps.hasOwnProperty('constructor')) { | ||
if (protoProps && protoProps.hasOwnProperty("constructor")) { | ||
child = protoProps.constructor; | ||
} else { | ||
child = function(){ parent.apply(this, arguments); }; | ||
child = function () { | ||
parent.apply(this, arguments); | ||
}; | ||
} | ||
// Set the prototype chain to inherit from `Parent` | ||
child.prototype = Object.create(parent.prototype) | ||
child.prototype = Object.create(parent.prototype); | ||
if (protoProps) { | ||
var i = -1, keys = Object.keys(protoProps) | ||
var i = -1, | ||
keys = Object.keys(protoProps); | ||
while (++i < keys.length) { | ||
var key = keys[i] | ||
child.prototype[key] = protoProps[key] | ||
} | ||
var key = keys[i]; | ||
child.prototype[key] = protoProps[key]; | ||
} | ||
} | ||
if (staticProps) { | ||
keys = Object.keys(staticProps) | ||
i = -1 | ||
var i = -1, | ||
keys = Object.keys(staticProps); | ||
while (++i < keys.length) { | ||
var key = keys[i] | ||
child[key] = staticProps[key] | ||
var key = keys[i]; | ||
child[key] = staticProps[key]; | ||
} | ||
@@ -41,3 +46,3 @@ } | ||
// Add static properties to the constructor function, if supplied. | ||
child.__proto__ = parent | ||
child.__proto__ = parent; | ||
@@ -49,2 +54,2 @@ // If there is an "extended" function set on the parent, | ||
return child; | ||
}; | ||
}; |
// Helpers | ||
// --------------- | ||
var _ = require('lodash'); | ||
var chalk = require('chalk') | ||
'use strict'; | ||
var _ = require('lodash'); | ||
var chalk = require('chalk'); | ||
var helpers = { | ||
// Sets the constraints necessary during a `model.save` call. | ||
saveConstraints: function(model, relatedData) { | ||
saveConstraints: function saveConstraints(model, relatedData) { | ||
var data = {}; | ||
if (relatedData && relatedData.type && relatedData.type !== 'belongsToMany' && relatedData.type !== 'belongsTo') { | ||
if (relatedData && !relatedData.isThrough() && relatedData.type !== 'belongsToMany' && relatedData.type !== 'belongsTo') { | ||
data[relatedData.key('foreignKey')] = relatedData.parentFk || model.get(relatedData.key('foreignKey')); | ||
if (relatedData.isMorph()) data[relatedData.key('morphKey')] = relatedData.key('morphValue'); | ||
} | ||
return model.set(data); | ||
return model.set(model.parse(data)); | ||
}, | ||
@@ -20,5 +22,5 @@ | ||
// an error if none is matched. | ||
morphCandidate: function(candidates, foreignTable) { | ||
var Target = _.find(candidates, function(Candidate) { | ||
return (_.result(Candidate.prototype, 'tableName') === foreignTable); | ||
morphCandidate: function morphCandidate(candidates, foreignTable) { | ||
var Target = _.find(candidates, function (Candidate) { | ||
return _.result(Candidate.prototype, 'tableName') === foreignTable; | ||
}); | ||
@@ -36,14 +38,32 @@ if (!Target) { | ||
// methods, and the values are the arguments for the query. | ||
query: function(obj, args) { | ||
obj._knex = obj._knex || obj._builder(_.result(obj, 'tableName')); | ||
query: function query(obj, args) { | ||
// Ensure the object has a query builder. | ||
if (!obj._knex) { | ||
var tableName = _.result(obj, 'tableName'); | ||
obj._knex = obj._builder(tableName); | ||
} | ||
// If there are no arguments, return the query builder. | ||
if (args.length === 0) return obj._knex; | ||
var method = args[0]; | ||
if (_.isFunction(method)) { | ||
// `method` is a query builder callback. Call it on the query builder | ||
// object. | ||
method.call(obj._knex, obj._knex); | ||
} else if (_.isObject(method)) { | ||
// `method` is an object. Use keys as methods and values as arguments to | ||
// the query builder. | ||
for (var key in method) { | ||
var target = _.isArray(method[key]) ? method[key] : [method[key]]; | ||
var target = _.isArray(method[key]) ? method[key] : [method[key]]; | ||
obj._knex[key].apply(obj._knex, target); | ||
} | ||
} else { | ||
// Otherwise assume that the `method` is string name of a query builder | ||
// method, and use the remaining args as arguments to that method. | ||
obj._knex[method].apply(obj._knex, args.slice(1)); | ||
@@ -54,12 +74,12 @@ } | ||
error: function(msg) { | ||
console.log(chalk.red(msg)) | ||
error: function error(msg) { | ||
console.log(chalk.red(msg)); | ||
}, | ||
warn: function(msg) { | ||
console.log(chalk.yellow(msg)) | ||
warn: function warn(msg) { | ||
console.log(chalk.yellow(msg)); | ||
}, | ||
deprecate: function(a, b) { | ||
helpers.warn(a + ' has been deprecated, please use ' + b + ' instead') | ||
deprecate: function deprecate(a, b) { | ||
helpers.warn(a + ' has been deprecated, please use ' + b + ' instead'); | ||
} | ||
@@ -69,3 +89,2 @@ | ||
module.exports = helpers | ||
module.exports = helpers; |
1312
lib/model.js
@@ -1,39 +0,297 @@ | ||
// Model | ||
// --------------- | ||
var _ = require('lodash'); | ||
var createError = require('create-error') | ||
'use strict'; | ||
var Sync = require('./sync'); | ||
var Helpers = require('./helpers'); | ||
var EagerRelation = require('./eager'); | ||
var Errors = require('./errors'); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } | ||
var ModelBase = require('./base/model'); | ||
var Promise = require('./base/promise'); | ||
var _lodash = require('lodash'); | ||
var BookshelfModel = ModelBase.extend({ | ||
var _lodash2 = _interopRequireDefault(_lodash); | ||
// The `hasOne` relation specifies that this table has exactly one of another type of object, | ||
// specified by a foreign key in the other table. The foreign key is assumed to be the singular of this | ||
// object's `tableName` with an `_id` suffix, but a custom `foreignKey` attribute may also be specified. | ||
hasOne: function(Target, foreignKey) { | ||
return this._relation('hasOne', Target, {foreignKey: foreignKey}).init(this); | ||
var _createError = require('create-error'); | ||
var _createError2 = _interopRequireDefault(_createError); | ||
var _sync = require('./sync'); | ||
var _sync2 = _interopRequireDefault(_sync); | ||
var _helpers = require('./helpers'); | ||
var _helpers2 = _interopRequireDefault(_helpers); | ||
var _eager = require('./eager'); | ||
var _eager2 = _interopRequireDefault(_eager); | ||
var _errors = require('./errors'); | ||
var _errors2 = _interopRequireDefault(_errors); | ||
var _baseModel = require('./base/model'); | ||
var _baseModel2 = _interopRequireDefault(_baseModel); | ||
var _basePromise = require('./base/promise'); | ||
var _basePromise2 = _interopRequireDefault(_basePromise); | ||
/** | ||
* @class Model | ||
* @extends ModelBase | ||
* @inheritdoc | ||
* @classdesc | ||
* | ||
* Models are simple objects representing individual database rows, specifying | ||
* the tableName 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. | ||
* | ||
* @constructor | ||
* @description | ||
* | ||
* When creating an instance of a model, you can pass in the initial values of | ||
* the attributes, which will be {@link Model#set set} on the | ||
* model. If you define an {@link 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 | ||
* {@link Model#constructor constructor}, which allows you to replace the | ||
* actual constructor function for your model. | ||
* | ||
* let Books = bookshelf.Model.extend({ | ||
* tableName: 'documents', | ||
* constructor: function() { | ||
* bookshelf.Model.apply(this, arguments); | ||
* this.on('saving', function(model, attrs, options) { | ||
* options.query.where('type', '=', 'book'); | ||
* }); | ||
* } | ||
* }); | ||
* | ||
* @param {Object} attributes Initial values for this model's attributes. | ||
* @param {Object=} options Hash of options. | ||
* @param {string=} options.tableName Initial value for {@link Model#tableName tableName}. | ||
* @param {boolean=} [options.hasTimestamps=false] | ||
* | ||
* Initial value for {@link Model#hasTimestamps hasTimestamps}. | ||
* | ||
* @param {boolean} [options.parse=false] | ||
* | ||
* Convert attributes by {@link Model#parse parse} before being {@link | ||
* Model#set set} on the model. | ||
* | ||
*/ | ||
var BookshelfModel = _baseModel2['default'].extend({ | ||
/** | ||
* The `hasOne` relation specifies that this table has exactly one of another | ||
* type of object, specified by a foreign key in the other table. | ||
* | ||
* let Record = bookshelf.Model.extend({ | ||
* tableName: 'health_records' | ||
* }); | ||
* | ||
* let Patient = bookshelf.Model.extend({ | ||
* tableName: 'patients', | ||
* record: function() { | ||
* return this.hasOne(Record); | ||
* } | ||
* }); | ||
* | ||
* // select * from `health_records` where `patient_id` = 1; | ||
* new Patient({id: 1}).related('record').fetch().then(function(model) { | ||
* ... | ||
* }); | ||
* | ||
* // alternatively, if you don't need the relation loaded on the patient's relations hash: | ||
* new Patient({id: 1}).record().fetch().then(function(model) { | ||
* ... | ||
* }); | ||
* | ||
* @method Model#hasOne | ||
* | ||
* @param {Model} Target | ||
* | ||
* Constructor of {@link Model} targeted by join. | ||
* | ||
* @param {string=} foreignKey | ||
* | ||
* ForeignKey in the `Target` model. By default, the `foreignKey` is assumed to | ||
* be the singular form of this model's {@link Model#tableName tableName}, | ||
* followed by `_id` / `_{{{@link ModelBase#idAttribute idAttribute}}}`. | ||
* | ||
* @returns {Model} | ||
*/ | ||
hasOne: function hasOne(Target, foreignKey) { | ||
return this._relation('hasOne', Target, { foreignKey: foreignKey }).init(this); | ||
}, | ||
// The `hasMany` relation specifies that this object has one or more rows in another table which | ||
// match on this object's primary key. The foreign key is assumed to be the singular of this object's | ||
// `tableName` with an `_id` suffix, but a custom `foreignKey` attribute may also be specified. | ||
hasMany: function(Target, foreignKey) { | ||
return this._relation('hasMany', Target, {foreignKey: foreignKey}).init(this); | ||
/** | ||
* The `hasMany` relation specifies that this model has one or more rows in | ||
* another table which match on this model's primary key. | ||
* | ||
* @method Model#hasMany | ||
* | ||
* @param {Model} Target | ||
* | ||
* Constructor of {@link Model} targeted by join. | ||
* | ||
* @param {string=} foreignKey | ||
* | ||
* ForeignKey in the `Target` model. By default, the foreignKey is assumed to | ||
* be the singular form of this model's tableName, followed by `_id` / | ||
* `_{{{@link ModelBase#idAttribute idAttribute}}}`. | ||
* | ||
* @returns {Collection} | ||
*/ | ||
hasMany: function hasMany(Target, foreignKey) { | ||
return this._relation('hasMany', Target, { foreignKey: foreignKey }).init(this); | ||
}, | ||
// A reverse `hasOne` relation, the `belongsTo`, where the specified key in this table | ||
// matches the primary `idAttribute` of another table. | ||
belongsTo: function(Target, foreignKey) { | ||
return this._relation('belongsTo', Target, {foreignKey: foreignKey}).init(this); | ||
/** | ||
* The `belongsTo` relationship is used when a model is a member of | ||
* another `Target` model. | ||
* | ||
* It can be used in a {@tutorial one-to-one} associations as the inverse | ||
* of a {@link Model#hasOne hasOne}. It can also used in {@tutorial | ||
* one-to-many} associations as the inverse of a {@link Model#hasMany hasMany} | ||
* (and is the one side of that association). In both cases, the {@link | ||
* Model#belongsTo belongsTo} relationship is used for a model that is a | ||
* member of another Target model, referenced by the foreignKey in the current | ||
* model. | ||
* | ||
* let Book = bookshelf.Model.extend({ | ||
* tableName: 'books', | ||
* author: function() { | ||
* return this.belongsTo(Author); | ||
* } | ||
* }); | ||
* | ||
* // select * from `books` where id = 1 | ||
* // select * from `authors` where id = book.author_id | ||
* Book.where({id: 1}).fetch({withRelated: ['author']}).then(function(book) { | ||
* console.log(JSON.stringify(book.related('author'))); | ||
* }); | ||
* | ||
* @method Model#belongsTo | ||
* | ||
* @param {Model} Target | ||
* | ||
* Constructor of {@link Model} targeted by join. | ||
* | ||
* @param {string=} foreignKey | ||
* | ||
* ForeignKey in this model. By default, the foreignKey is assumed to | ||
* be the singular form of the `Target` model's tableName, followed by `_id` / | ||
* `_{{{@link ModelBase#idAttribute idAttribute}}}`. | ||
* | ||
* @returns {Model} | ||
*/ | ||
belongsTo: function belongsTo(Target, foreignKey) { | ||
return this._relation('belongsTo', Target, { foreignKey: foreignKey }).init(this); | ||
}, | ||
// A `belongsToMany` relation is when there are many-to-many relation | ||
// between two models, with a joining table. | ||
belongsToMany: function(Target, joinTableName, foreignKey, otherKey) { | ||
/** | ||
* Defines a many-to-many relation, where the current model is joined to one | ||
* or 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. | ||
* | ||
* let Account = bookshelf.Model.extend({ | ||
* tableName: 'accounts' | ||
* }); | ||
* | ||
* let User = bookshelf.Model.extend({ | ||
* | ||
* tableName: 'users', | ||
* | ||
* allAccounts: function () { | ||
* return this.belongsToMany(Account); | ||
* }, | ||
* | ||
* adminAccounts: function() { | ||
* return this.belongsToMany(Account).query({where: {access: 'admin'}}); | ||
* }, | ||
* | ||
* viewAccounts: function() { | ||
* return this.belongsToMany(Account).query({where: {access: 'readonly'}}); | ||
* } | ||
* | ||
* }); | ||
* | ||
* The default key names in the joining table are the singular versions of the | ||
* model table names, followed by `_id` / | ||
* _{{{@link ModelBase#idAttribute idAttribute}}}. So in the above case, the | ||
* columns in the joining table | ||
* would be `user_id`, `account_id`, and `access`, which is used as an | ||
* example of how dynamic relations can be formed using different contexts. | ||
* To customize the keys used in, or the {@link Model#tableName tableName} | ||
* used for the join table, you may specify them like so: | ||
* | ||
* this.belongsToMany(Account, 'users_accounts', 'userid', 'accountid'); | ||
* | ||
* If you wish to create a {@link Model#belongsToMany belongsToMany} | ||
* association where the joining table has a primary key, and more information | ||
* about the model, you may create a {@link Model#belongsToMany belongsToMany} | ||
* {@link Relation#through through} relation: | ||
* | ||
* let Doctor = bookshelf.Model.extend({ | ||
* | ||
* patients: function() { | ||
* return this.belongsToMany(Patient).through(Appointment); | ||
* } | ||
* | ||
* }); | ||
* | ||
* let Appointment = bookshelf.Model.extend({ | ||
* | ||
* patient: function() { | ||
* return this.belongsTo(Patient); | ||
* }, | ||
* | ||
* doctor: function() { | ||
* return this.belongsTo(Doctor); | ||
* } | ||
* | ||
* }); | ||
* | ||
* let Patient = bookshelf.Model.extend({ | ||
* | ||
* doctors: function() { | ||
* return this.belongsToMany(Doctor).through(Appointment); | ||
* } | ||
* | ||
* }); | ||
* | ||
* @belongsTo Model | ||
* @method Model#belongsToMany | ||
* @param {Model} Target | ||
* | ||
* Constructor of {@link Model} targeted by join. | ||
* | ||
* @param {string=} foreignKey | ||
* | ||
* Foreign key in this model. By default, the `foreignKey` is assumed to | ||
* be the singular form of the `Target` model's tableName, followed by `_id` / | ||
* `_{{{@link ModelBase#idAttribute idAttribute}}}`. | ||
* | ||
* @param {string=} table | ||
* | ||
* Name of the joining table. Defaults to the two table names, joined by an | ||
* underscore, ordered alphabetically. | ||
* | ||
* @param {string=} otherKey | ||
* | ||
* Foreign key in the `Target` model. By default, the `otherKey` is assumed to | ||
* be the singular form of this model's tableName, followed by `_id` / | ||
* `_{{{@link ModelBase#idAttribute idAttribute}}}`. | ||
* | ||
* @returns {Collection} | ||
*/ | ||
belongsToMany: function belongsToMany(Target, joinTableName, foreignKey, otherKey) { | ||
return this._relation('belongsToMany', Target, { | ||
@@ -44,81 +302,367 @@ joinTableName: joinTableName, foreignKey: foreignKey, otherKey: otherKey | ||
// A `morphOne` relation is a one-to-one polymorphic association from this model | ||
// to another model. | ||
morphOne: function(Target, name, columnNames, morphValue) { | ||
/** | ||
* The {@link Model#morphOne morphOne} is used to signify a {@link oneToOne | ||
* one-to-one} {@link polymorphicRelation polymorphic relation} with | ||
* another `Target` model, where the `name` of the model is used to determine | ||
* which database table keys are used. The naming convention requires the | ||
* `name` prefix an `_id` and `_type` field in the database. So for the case | ||
* below the table names would be `imageable_type` and `imageable_id`. The | ||
* `morphValue` may be optionally set to store/retrieve a different value in | ||
* the `_type` column than the {@link Model#tableName}. | ||
* | ||
* let Site = bookshelf.Model.extend({ | ||
* tableName: 'sites', | ||
* photo: function() { | ||
* return this.morphOne(Photo, 'imageable'); | ||
* } | ||
* }); | ||
* | ||
* And with custom `columnNames`: | ||
* | ||
* let Site = bookshelf.Model.extend({ | ||
* tableName: 'sites', | ||
* photo: function() { | ||
* return this.morphOne(Photo, 'imageable', ["ImageableType", "ImageableId"]); | ||
* } | ||
* }); | ||
* | ||
* Note that both `columnNames` and `morphValue` are optional arguments. How | ||
* your argument is treated when only one is specified, depends on the type. | ||
* If your argument is an array, it will be assumed to contain custom | ||
* `columnNames`. If it's not, it will be assumed to indicate a `morphValue`. | ||
* | ||
* @method Model#morphOne | ||
* | ||
* @param {Model} Target Constructor of {@link Model} targeted by join. | ||
* @param {string=} name Prefix for `_id` and `_type` columns. | ||
* @param {(string[])=} columnNames | ||
* | ||
* Array containing two column names, the first is the `_type`, the second | ||
* is the `_id`. | ||
* | ||
* @param {string=} [morphValue=Target#{@link Model#tableName tableName}] | ||
* | ||
* The string value associated with this relationship. Stored in the `_type` | ||
* column of the polymorphic table. Defaults to `Target#{@link | ||
* Model#tableName tableName}`. | ||
* | ||
* @returns {Model} The related model. | ||
*/ | ||
morphOne: function morphOne(Target, name, columnNames, morphValue) { | ||
return this._morphOneOrMany(Target, name, columnNames, morphValue, 'morphOne'); | ||
}, | ||
// A `morphMany` relation is a polymorphic many-to-one relation from this model | ||
// to many another models. | ||
morphMany: function(Target, name, columnNames, morphValue) { | ||
/** | ||
* {@link Model#morphMany morphMany} is essentially the same as a {@link | ||
* Model#morphOne morphOne}, but creating a {@link Collection collection} | ||
* rather than a {@link Model model} (similar to a {@link Model#hasOne | ||
* hasOne} vs. {@link Model#hasMany hasMany} relation). | ||
* | ||
* {@link Model#morphMany morphMany} is used to signify a {@link oneToMany | ||
* one-to-many} or {@link manyToMany many-to-many} {@link polymorphicRelation | ||
* polymorphic relation} with another `Target` model, where the `name` of the | ||
* model is used to determine which database table keys are used. The naming | ||
* convention requires the `name` prefix an `_id` and `_type` field in the | ||
* database. So for the case below the table names would be `imageable_type` | ||
* and `imageable_id`. The `morphValue` may be optionally set to | ||
* store/retrieve a different value in the `_type` column than the `Target`'s | ||
* {@link Model#tableName tableName}. | ||
* | ||
* let Post = bookshelf.Model.extend({ | ||
* tableName: 'posts', | ||
* photos: function() { | ||
* return this.morphMany(Photo, 'imageable'); | ||
* } | ||
* }); | ||
* | ||
* And with custom columnNames: | ||
* | ||
* let Post = bookshelf.Model.extend({ | ||
* tableName: 'posts', | ||
* photos: function() { | ||
* return this.morphMany(Photo, 'imageable', ["ImageableType", "ImageableId"]); | ||
* } | ||
* }); | ||
* | ||
* @method Model#morphMany | ||
* | ||
* @param {Model} Target Constructor of {@link Model} targeted by join. | ||
* @param {string=} name Prefix for `_id` and `_type` columns. | ||
* @param {(string[])=} columnNames | ||
* | ||
* Array containing two column names, the first is the `_type`, the second is the `_id`. | ||
* | ||
* @param {string=} [morphValue=Target#{@link Model#tableName tablename}] | ||
* | ||
* The string value associated with this relationship. Stored in the `_type` | ||
* column of the polymorphic table. Defaults to `Target`#{@link Model#tableName | ||
* tablename}. | ||
* | ||
* @returns {Collection} A collection of related models. | ||
*/ | ||
morphMany: function morphMany(Target, name, columnNames, morphValue) { | ||
return this._morphOneOrMany(Target, name, columnNames, morphValue, 'morphMany'); | ||
}, | ||
// Defines the opposite end of a `morphOne` or `morphMany` relationship, where | ||
// the alternate end of the polymorphic model is defined. | ||
morphTo: function(morphName) { | ||
var columnNames, remainder; | ||
if (!_.isString(morphName)) throw new Error('The `morphTo` name must be specified.'); | ||
if (_.isArray(arguments[1])) { | ||
/** | ||
* The {@link Model#morphTo morphTo} relation is used to specify the inverse | ||
* of the {@link Model#morphOne morphOne} or {@link Model#morphMany | ||
* morphMany} relations, where the `targets` must be passed to signify which | ||
* {@link Model models} are the potential opposite end of the {@link | ||
* polymorphicRelation polymorphic relation}. | ||
* | ||
* let Photo = bookshelf.Model.extend({ | ||
* tableName: 'photos', | ||
* imageable: function() { | ||
* return this.morphTo('imageable', Site, Post); | ||
* } | ||
* }); | ||
* | ||
* And with custom columnNames: | ||
* | ||
* let Photo = bookshelf.Model.extend({ | ||
* tableName: 'photos', | ||
* imageable: function() { | ||
* return this.morphTo('imageable', ["ImageableType", "ImageableId"], Site, Post); | ||
* } | ||
* }); | ||
* | ||
* @method Model#morphTo | ||
* | ||
* @param {string} name Prefix for `_id` and `_type` columns. | ||
* @param {(string[])=} columnNames | ||
* | ||
* Array containing two column names, the first is the `_type`, the second is the `_id`. | ||
* | ||
* @param {...Model} Target Constructor of {@link Model} targeted by join. | ||
* | ||
* @returns {Model} | ||
*/ | ||
morphTo: function morphTo(morphName) { | ||
if (!_lodash2['default'].isString(morphName)) throw new Error('The `morphTo` name must be specified.'); | ||
var columnNames = undefined, | ||
candidates = undefined; | ||
if (_lodash2['default'].isArray(arguments[1])) { | ||
columnNames = arguments[1]; | ||
remainder = _.rest(arguments, 2); | ||
candidates = _lodash2['default'].rest(arguments, 2); | ||
} else { | ||
columnNames = null; | ||
remainder = _.rest(arguments); | ||
candidates = _lodash2['default'].rest(arguments); | ||
} | ||
return this._relation('morphTo', null, {morphName: morphName, columnNames: columnNames, candidates: remainder}).init(this); | ||
return this._relation('morphTo', null, { morphName: morphName, columnNames: columnNames, candidates: candidates }).init(this); | ||
}, | ||
// Used to define passthrough relationships - `hasOne`, `hasMany`, | ||
// `belongsTo` or `belongsToMany`, "through" a `Interim` model or collection. | ||
through: function(Interim, foreignKey, otherKey) { | ||
return this.relatedData.through(this, Interim, {throughForeignKey: foreignKey, otherKey: otherKey}); | ||
/** | ||
* Helps to create dynamic relations between {@link Model models} and {@link | ||
* Collection collections}, where a {@link Model#hasOne hasOne}, {@link | ||
* Model#hasMany hasMany}, {@link Model#belongsTo belongsTo}, or {@link | ||
* Model#belongsToMany belongsToMany} relation may run through a `JoinModel`. | ||
* | ||
* A good example of where this would be useful is if a book {@link | ||
* Model#hasMany hasMany} paragraphs through chapters. Consider the following examples: | ||
* | ||
* | ||
* let Book = bookshelf.Model.extend({ | ||
* | ||
* tableName: 'books', | ||
* | ||
* // Find all paragraphs associated with this book, by | ||
* // passing through the "Chapter" model. | ||
* paragraphs: function() { | ||
* return this.hasMany(Paragraph).through(Chapter); | ||
* }, | ||
* | ||
* chapters: function() { | ||
* return this.hasMany(Chapter); | ||
* } | ||
* | ||
* }); | ||
* | ||
* let Chapter = bookshelf.Model.extend({ | ||
* | ||
* tableName: 'chapters', | ||
* | ||
* paragraphs: function() { | ||
* return this.hasMany(Paragraph); | ||
* } | ||
* | ||
* }); | ||
* | ||
* let Paragraph = bookshelf.Model.extend({ | ||
* | ||
* tableName: 'paragraphs', | ||
* | ||
* chapter: function() { | ||
* return this.belongsTo(Chapter); | ||
* }, | ||
* | ||
* // A reverse relation, where we can get the book from the chapter. | ||
* book: function() { | ||
* return this.belongsTo(Book).through(Chapter); | ||
* } | ||
* | ||
* }); | ||
* | ||
* The "through" table creates a pivot model, which it assigns to {@link | ||
* Model#pivot model.pivot} after it is created. On {@link Model#toJSON | ||
* toJSON}, the pivot model is flattened to values prefixed with | ||
* `_pivot_`. | ||
* | ||
* @method Model#through | ||
* @param {Model} Interim Pivot model. | ||
* @param {string=} throughForeignKey | ||
* | ||
* Foreign key in this model. By default, the `foreignKey` is assumed to | ||
* be the singular form of the `Target` model's tableName, followed by `_id` / | ||
* `_{{{@link ModelBase#idAttribute idAttribute}}}`. | ||
* | ||
* @param {string=} otherKey | ||
* | ||
* Foreign key in the `Interim` model. By default, the `otherKey` is assumed to | ||
* be the singular form of this model's tableName, followed by `_id` / | ||
* `_{{{@link ModelBase#idAttribute idAttribute}}}`. | ||
* | ||
* @returns {Collection} | ||
*/ | ||
through: function through(Interim, throughForeignKey, otherKey) { | ||
return this.relatedData.through(this, Interim, { throughForeignKey: throughForeignKey, otherKey: otherKey }); | ||
}, | ||
// Fetch a model based on the currently set attributes, | ||
// returning a model to the callback, along with any options. | ||
// Returns a deferred promise through the `Bookshelf.Sync`. | ||
// If `{require: true}` is set as an option, the fetch is considered | ||
// a failure if the model comes up blank. | ||
fetch: Promise.method(function(options) { | ||
options = options ? _.clone(options) : {}; | ||
// Update the attributes of a model, fetching it by its primary key. If | ||
// no attribute matches its `idAttribute`, then fetch by all available | ||
// fields. | ||
refresh: function refresh(options) { | ||
// If this is new, we use all its attributes. Otherwise we just grab the | ||
// primary key. | ||
var attributes = this.isNew() ? this.attributes : _lodash2['default'].pick(this.attributes, this.idAttribute); | ||
return this._doFetch(attributes, options); | ||
}, | ||
/** | ||
* Fetches a {@link Model model} from the database, using any {@link | ||
* Model#attributes attributes} currently set on the model to form a `select` | ||
* query. | ||
* | ||
* A {@link Model#fetching "fetching"} event will be fired just before the | ||
* record is fetched; a good place to hook into for validation. {@link | ||
* Model#fetched "fetched"} event will be fired when a record is successfully | ||
* retrieved. | ||
* | ||
* If you need to constrain the query | ||
* performed by fetch, you can call {@link Model#query query} before calling | ||
* {@link Model#fetch fetch}. | ||
* | ||
* // select * from `books` where `ISBN-13` = '9780440180296' | ||
* new Book({'ISBN-13': '9780440180296'}) | ||
* .fetch() | ||
* .then(function(model) { | ||
* // outputs 'Slaughterhouse Five' | ||
* console.log(model.get('title')); | ||
* }); | ||
* | ||
* _If you'd like to only fetch specific columns, you may specify a `columns` | ||
* property in the `options` for the {@link Model#fetch fetch} call, or use | ||
* {@link Model#query query}, tapping into the {@link Knex} {@link | ||
* Knex#column column} method to specify which columns will be fetched._ | ||
* | ||
* The `withRelated` parameter may be specified to fetch the resource, along | ||
* with any specified {@link Model#relations 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 {@link Model#relations relations} property on the model, may | ||
* be retrieved with the {@link Model#related related} method, and will be | ||
* serialized as properties on a {@link Model#toJSON toJSON} call unless | ||
* `{shallow: true}` is passed. | ||
* | ||
* let 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(book) { | ||
* console.log(book.related('genre').toJSON()); | ||
* console.log(book.related('editions').toJSON()); | ||
* console.log(book.toJSON()); | ||
* }); | ||
* | ||
* @method Model#fetch | ||
* | ||
* @param {Object=} options - Hash of options. | ||
* @param {boolean=} [options.require=false] | ||
* If `true`, will reject the returned response with a {@link | ||
* Model.NotFoundError NotFoundError} if no result is found. | ||
* @param {(string|string[])=} [options.columns='*'] | ||
* Limit the number of columns fetched. | ||
* @param {Transaction=} options.transacting | ||
* Optionally run the query in a transaction. | ||
* | ||
* @fires Model#fetching | ||
* @fires Model#fetched | ||
* | ||
* @throws {Model.NotFoundError} | ||
* | ||
* @returns {Promise<Model|undefined>} | ||
* A promise resolving to the fetched {@link Model model} or `undefined` if none exists. | ||
* | ||
*/ | ||
fetch: function fetch(options) { | ||
// Fetch uses all set attributes. | ||
return this._doFetch(this.attributes, options); | ||
}, | ||
_doFetch: _basePromise2['default'].method(function (attributes, options) { | ||
options = options ? _lodash2['default'].clone(options) : {}; | ||
// Run the `first` call on the `sync` object to fetch a single model. | ||
return this.sync(options) | ||
.first() | ||
.bind(this) | ||
return this.sync(options).first(attributes).bind(this) | ||
// Jump the rest of the chain if the response doesn't exist... | ||
.tap(function(response) { | ||
if (!response || response.length === 0) { | ||
if (options.require) throw new this.constructor.NotFoundError('EmptyResponse'); | ||
return Promise.reject(null); | ||
} | ||
}) | ||
// Jump the rest of the chain if the response doesn't exist... | ||
.tap(function (response) { | ||
if (!response || response.length === 0) { | ||
if (options.require) throw new this.constructor.NotFoundError('EmptyResponse'); | ||
return _basePromise2['default'].reject(null); | ||
} | ||
}) | ||
// Now, load all of the data into the model as necessary. | ||
.tap(this._handleResponse) | ||
// Now, load all of the data into the model as necessary. | ||
.tap(this._handleResponse) | ||
// If the "withRelated" is specified, we also need to eager load all of the | ||
// data on the model, as a side-effect, before we ultimately jump into the | ||
// next step of the model. Since the `columns` are only relevant to the current | ||
// level, ensure those are omitted from the options. | ||
.tap(function(response) { | ||
if (options.withRelated) { | ||
return this._handleEager(response, _.omit(options, 'columns')); | ||
} | ||
}) | ||
// If the "withRelated" is specified, we also need to eager load all of the | ||
// data on the model, as a side-effect, before we ultimately jump into the | ||
// next step of the model. Since the `columns` are only relevant to the current | ||
// level, ensure those are omitted from the options. | ||
.tap(function (response) { | ||
if (options.withRelated) { | ||
return this._handleEager(response, _lodash2['default'].omit(options, 'columns')); | ||
} | ||
}).tap(function (response) { | ||
.tap(function(response) { | ||
return this.triggerThen('fetched', this, response, options); | ||
}) | ||
.return(this) | ||
.catch(function(err) { | ||
if (err === null) return err; | ||
throw err; | ||
}); | ||
/** | ||
* Fired after a `fetch` operation. A promise may be returned from the | ||
* event handler for async behaviour. | ||
* | ||
* @event Model#fetched | ||
* @param {Model} model The model firing the event. | ||
* @param {Object} reponse Knex query response. | ||
* @param {Object} options Options object passed to {@link Model#fetch fetch}. | ||
* @returns {Promise} | ||
*/ | ||
return this.triggerThen('fetched', this, response, options); | ||
})['return'](this)['catch'](function (err) { | ||
if (err === null) return err; | ||
throw err; | ||
}); | ||
}), | ||
// Shortcut for creating a collection and fetching the associated models. | ||
fetchAll: function(options) { | ||
all: function all() { | ||
var collection = this.constructor.collection(); | ||
@@ -128,73 +672,227 @@ collection._knex = this.query().clone(); | ||
if (this.relatedData) collection.relatedData = this.relatedData; | ||
var model = this; | ||
return collection | ||
.on('fetching', function(collection, columns, options) { | ||
return model.triggerThen('fetching:collection', collection, columns, options); | ||
}) | ||
.on('fetched', function(collection, resp, options) { | ||
return model.triggerThen('fetched:collection', collection, resp, options); | ||
}) | ||
.fetch(options); | ||
return collection; | ||
}, | ||
// Eager loads relationships onto an already populated `Model` instance. | ||
load: Promise.method(function(relations, options) { | ||
return Promise.bind(this) | ||
.then(function() { | ||
return [this.format(_.extend(Object.create(null), this.attributes))]; | ||
}) | ||
.then(function(response) { | ||
return this._handleEager(response, _.extend({}, options, { | ||
shallow: true, | ||
withRelated: _.isArray(relations) ? relations : [relations] | ||
})); | ||
}) | ||
.return(this); | ||
count: function count(column, options) { | ||
return this.all().count(column, options); | ||
}, | ||
/** | ||
* Fetches a collection of {@link Model models} from the database, using any | ||
* query parameters currently set on the model to form a select query. Returns | ||
* a promise, which will resolve with the fetched collection. If you wish to | ||
* trigger an error if no models are found, pass {require: true} as one of | ||
* the options to the `fetchAll` call. | ||
* | ||
* If you need to constrain the query performed by fetch, you can call the | ||
* {@link Model#query query} method before calling fetch. | ||
* | ||
* @method Model#fetchAll | ||
* | ||
* @param {Object=} options - Hash of options. | ||
* @param {boolean=} [options.require=false] | ||
* | ||
* Rejects the returned promise with an `EmptyError` if no records are returned. | ||
* | ||
* @param {Transaction=} options.transacting | ||
* | ||
* Optionally run the query in a transaction. | ||
* | ||
* @fires Model#"fetching:collection" | ||
* @fires Model#"fetched:collection" | ||
* | ||
* @throws {Model.EmptyError} | ||
* | ||
* Rejects the promise in the event of an empty response if the `require: true` option. | ||
* | ||
* @returns {Promise<Collection>} A promise resolving to the fetched {@link Collection collection}. | ||
* | ||
*/ | ||
fetchAll: function fetchAll(options) { | ||
var _this = this; | ||
var collection = this.all(); | ||
return collection.once('fetching', function (__, columns, opts) { | ||
/** | ||
* Fired before a {@link Model#fetchAll fetchAll} operation. A promise | ||
* may be returned from the event handler for async behaviour. | ||
* | ||
* @event Model#"fetching:collection" | ||
* @param {Model} collection The collection that has been fetched. | ||
* @param {string[]} columns The columns being retrieved by the query. | ||
* @param {Object} options Options object passed to {@link Model#fetchAll fetchAll}. | ||
* @returns {Promise} | ||
*/ | ||
return _this.triggerThen('fetching:collection', collection, columns, opts); | ||
}).once('fetched', function (__, resp, opts) { | ||
/** | ||
* Fired after a {@link Model#fetchAll fetchAll} operation. A promise | ||
* may be returned from the event handler for async behaviour. | ||
* | ||
* @event Model#"fetched:collection" | ||
* @param {Model} collection The collection that has been fetched. | ||
* @param {Object} resp The Knex query response. | ||
* @param {Object} options Options object passed to {@link Model#fetchAll fetchAll}. | ||
* @returns {Promise} | ||
*/ | ||
return _this.triggerThen('fetched:collection', collection, resp, opts); | ||
}).fetch(options); | ||
}, | ||
/** | ||
* @method Model#load | ||
* @description | ||
* The load method takes an array of relations to eager load attributes onto a | ||
* {@link Model}, in a similar way that the `withRelated` property works on | ||
* {@link Model#fetch fetch}. Dot separated attributes may be used to specify deep | ||
* eager loading. | ||
* | ||
* @example | ||
* 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: [...]} | ||
* ] | ||
* } | ||
* | ||
* @param {string|string[]} relations The relation, or relations, to be loaded. | ||
* @param {Object=} options Hash of options. | ||
* @param {Transaction=} options.transacting | ||
* Optionally run the query in a transaction. | ||
* @returns {Promise<Model>} A promise resolving to this {@link Model model} | ||
*/ | ||
load: _basePromise2['default'].method(function (relations, options) { | ||
return _basePromise2['default'].bind(this).then(function () { | ||
return [this.format(_lodash2['default'].extend(Object.create(null), this.attributes))]; | ||
}).then(function (response) { | ||
return this._handleEager(response, _lodash2['default'].extend({}, options, { | ||
shallow: true, | ||
withRelated: _lodash2['default'].isArray(relations) ? relations : [relations] | ||
})); | ||
})['return'](this); | ||
}), | ||
// Sets and saves the hash of model attributes, triggering | ||
// a "creating" or "updating" event on the model, as well as a "saving" event, | ||
// to bind listeners for any necessary validation, logging, etc. | ||
// If an error is thrown during these events, the model will not be saved. | ||
save: Promise.method(function(key, val, options) { | ||
var attrs; | ||
/** | ||
* @method Model#save | ||
* @description | ||
* | ||
* `save` is used to perform either an insert or update query using the | ||
* model's set {@link Model#attributes attributes}. | ||
* | ||
* If the model {@link ModelBase#isNew isNew}, any {@link Model#defaults defaults} | ||
* will be set and an `insert` query will be performed. Otherwise it will | ||
* `update` the record with a corresponding ID. This behaviour can be overriden | ||
* with the `method` option. | ||
* | ||
* new Post({name: 'New Article'}).save().then(function(model) { | ||
* // ... | ||
* }); | ||
* | ||
* If you only wish to update with the params passed to the save, you may pass | ||
* a {patch: true} flag to the database: | ||
* | ||
* // update authors set "bio" = 'Short user bio' where "id" = 1 | ||
* new Author({id: 1, first_name: 'User'}) | ||
* .save({bio: 'Short user bio'}, {patch: true}) | ||
* .then(function(model) { | ||
* // ... | ||
* }); | ||
* | ||
* Several events fired on the model when saving: a {@link Model#creating | ||
* "creating"}, or {@link Model#updating "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 {@link Model#created "created"}, or {@link Model#"updated"} | ||
* event is fired after the model is saved, as well as a {@link Model#saved | ||
* "saved"} event either way. If you wish to modify the query when the {@link | ||
* Model#saving "saving"} event is fired, the knex query object should is | ||
* available in `options.query`. | ||
* | ||
* // Save with no arguments | ||
* Model.forge({id: 5, firstName: "John", lastName: "Smith"}).save().then(function() { //... | ||
* | ||
* // Or add attributes during save | ||
* Model.forge({id: 5}).save({firstName: "John", latName: "Smith"}).then(function() { //... | ||
* | ||
* // Or, if you prefer, for a single attribute | ||
* Model.forge({id: 5}).save('name', 'John Smith').then(function() { //... | ||
* | ||
* @param {string=} key Attribute name. | ||
* @param {string=} val Attribute value. | ||
* @param {Object=} attrs A has of attributes. | ||
* @param {Object=} options | ||
* @param {Transaction=} options.transacting | ||
* Optionally run the query in a transaction. | ||
* @param {string=} options.method | ||
* Explicitly select a save method, either `"update"` or `"insert"`. | ||
* @param {string} [options.defaults=false] | ||
* Assign {@link Model#defaults defaults} in an `update` operation. | ||
* @param {bool} [options.patch=false] | ||
* Only save attributes supplied in arguments to `save`. | ||
* @param {bool} [options.require=true] | ||
* Throw a {@link Model.NoRowsUpdatedError} if no records are affected by save. | ||
* | ||
* @fires Model#saving | ||
* @fires Model#creating | ||
* @fires Model#updating | ||
* @fires Model#created | ||
* @fires Model#updated | ||
* @fires Model#saved | ||
* | ||
* @throws {Model.NoRowsUpdatedError} | ||
* | ||
* @returns {Promise<Model>} A promise resolving to the saved and updated model. | ||
*/ | ||
save: _basePromise2['default'].method(function (key, val, options) { | ||
var attrs = undefined; | ||
// Handle both `"key", value` and `{key: value}` -style arguments. | ||
if (key == null || typeof key === "object") { | ||
if (key == null || typeof key === 'object') { | ||
attrs = key || {}; | ||
options = _.clone(val) || {}; | ||
options = _lodash2['default'].clone(val) || {}; | ||
} else { | ||
(attrs = {})[key] = val; | ||
options = options ? _.clone(options) : {}; | ||
options = options ? _lodash2['default'].clone(options) : {}; | ||
} | ||
return Promise.bind(this).then(function() { | ||
return this.isNew(options); | ||
}).then(function(isNew) { | ||
return _basePromise2['default'].bind(this).then(function () { | ||
return this.saveMethod(options); | ||
}).then(function (method) { | ||
// If the model has timestamp columns, | ||
// set them as attributes on the model, even | ||
// if the "patch" option is specified. | ||
if (this.hasTimestamps) _.extend(attrs, this.timestamp(options)); | ||
// Determine whether which kind of save we will do, update or insert. | ||
options.method = method; | ||
// Determine whether the model is new, based on whether the model has an `idAttribute` or not. | ||
options.method = (options.method || (isNew ? 'insert' : 'update')).toLowerCase(); | ||
var method = options.method; | ||
var vals = attrs; | ||
// If the object is being created, we merge any defaults here | ||
// rather than during object creation. | ||
// If the object is being created, we merge any defaults here rather than | ||
// during object creation. | ||
if (method === 'insert' || options.defaults) { | ||
var defaults = _.result(this, 'defaults'); | ||
var defaults = _lodash2['default'].result(this, 'defaults'); | ||
if (defaults) { | ||
vals = _.extend({}, defaults, this.attributes, vals); | ||
attrs = _lodash2['default'].extend({}, defaults, this.attributes, attrs); | ||
} | ||
} | ||
// Set the attributes on the model. | ||
this.set(vals, {silent: true}); | ||
// Set the attributes on the model. Note that we do this before adding | ||
// timestamps, as `timestamp` calls `set` internally. | ||
this.set(attrs, { silent: true }); | ||
// Now set timestamps if appropriate. Extend `attrs` so that the | ||
// timestamps will be provided for a patch operation. | ||
if (this.hasTimestamps) { | ||
_lodash2['default'].extend(attrs, this.timestamp({ method: method, silent: true })); | ||
} | ||
// If there are any save constraints, set them on the model. | ||
if (this.relatedData && this.relatedData.type !== 'morphTo') { | ||
Helpers.saveConstraints(this, this.relatedData); | ||
_helpers2['default'].saveConstraints(this, this.relatedData); | ||
} | ||
@@ -207,8 +905,46 @@ | ||
return this.triggerThen((method === 'insert' ? 'creating saving' : 'updating saving'), this, attrs, options) | ||
.bind(this) | ||
.then(function() { | ||
/** | ||
* Saving event. | ||
* | ||
* Fired before an `insert` or `update` query. A promise may be | ||
* returned from the event handler for async behaviour. Throwing an | ||
* exception from the handler will cancel the save. | ||
* | ||
* @event Model#saving | ||
* @param {Model} model The model firing the event. | ||
* @param {Object} attrs Model firing the event. | ||
* @param {Object} options Options object passed to {@link Model#save save}. | ||
* @returns {Promise} | ||
*/ | ||
/** | ||
* Creating event. | ||
* | ||
* Fired before `insert` query. A promise may be | ||
* returned from the event handler for async behaviour. Throwing an | ||
* exception from the handler will cancel the save operation. | ||
* | ||
* @event Model#creating | ||
* @param {Model} model The model firing the event. | ||
* @param {Object} attrs Model firing the event. | ||
* @param {Object} options Options object passed to {@link Model#save save}. | ||
* @returns {Promise} | ||
*/ | ||
/** | ||
* Updating event. | ||
* | ||
* Fired before `update` query. A promise may be | ||
* returned from the event handler for async behaviour. Throwing an | ||
* exception from the handler will cancel the save operation. | ||
* | ||
* @event Model#updating | ||
* @param {Model} model The model firing the event. | ||
* @param {Object} attrs Model firing the event. | ||
* @param {Object} options Options object passed to {@link Model#save save}. | ||
* @returns {Promise} | ||
*/ | ||
return this.triggerThen(method === 'insert' ? 'creating saving' : 'updating saving', this, attrs, options).bind(this).then(function () { | ||
return sync[options.method](method === 'update' && options.patch ? attrs : this.attributes); | ||
}) | ||
.then(function(resp) { | ||
}).then(function (resp) { | ||
@@ -230,21 +966,93 @@ // After a successful database save, the id is updated if the model was created | ||
return this.triggerThen((method === 'insert' ? 'created saved' : 'updated saved'), this, resp, options); | ||
/** | ||
* Saved event. | ||
* | ||
* Fired before after an `insert` or `update` query. | ||
* | ||
* @event Model#saved | ||
* @param {Model} model The model firing the event. | ||
* @param {Object} resp The database response. | ||
* @param {Object} options Options object passed to {@link Model#save save}. | ||
* @returns {Promise} | ||
*/ | ||
/** | ||
* Created event. | ||
* | ||
* Fired before after an `insert` query. | ||
* | ||
* @event Model#created | ||
* @param {Model} model The model firing the event. | ||
* @param {Object} attrs Model firing the event. | ||
* @param {Object} options Options object passed to {@link Model#save save}. | ||
* @returns {Promise} | ||
*/ | ||
/** | ||
* Updated event. | ||
* | ||
* Fired before after an `update` query. | ||
* | ||
* @event Model#updated | ||
* @param {Model} model The model firing the event. | ||
* @param {Object} attrs Model firing the event. | ||
* @param {Object} options Options object passed to {@link Model#save save}. | ||
* @returns {Promise} | ||
*/ | ||
return this.triggerThen(method === 'insert' ? 'created saved' : 'updated saved', this, resp, options); | ||
}); | ||
}) | ||
.return(this); | ||
})['return'](this); | ||
}), | ||
// Destroy a model, calling a "delete" based on its `idAttribute`. | ||
// A "destroying" and "destroyed" are triggered on the model before | ||
// and after the model is destroyed, respectively. If an error is thrown | ||
// during the "destroying" event, the model will not be destroyed. | ||
destroy: Promise.method(function(options) { | ||
options = options ? _.clone(options) : {}; | ||
/** | ||
* `destroy` performs a `delete` on the model, using the model's {@link | ||
* ModelBase#idAttribute idAttribute} to constrain the query. | ||
* | ||
* A {@link Model#destroying "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 {@link Model#destroyed "destroyed"} event is fired after the model's | ||
* removal is completed. | ||
* | ||
* @method Model#destroy | ||
* | ||
* @param {Object=} options Hash of options. | ||
* @param {Transaction=} options.transacting Optionally run the query in a transaction. | ||
* | ||
* @example | ||
* | ||
* new User({id: 1}) | ||
* .destroy() | ||
* .then(function(model) { | ||
* // ... | ||
* }); | ||
* | ||
* @fires Model#destroying | ||
* @fires Model#destroyed | ||
*/ | ||
destroy: _basePromise2['default'].method(function (options) { | ||
options = options ? _lodash2['default'].clone(options) : {}; | ||
var sync = this.sync(options); | ||
options.query = sync.query; | ||
return Promise.bind(this).then(function() { | ||
return _basePromise2['default'].bind(this).then(function () { | ||
/** | ||
* Destroying event. | ||
* | ||
* Fired before a `delete` query. A promise may be returned from the event | ||
* handler for async behaviour. Throwing an exception from the handler | ||
* will reject the promise and cancel the deletion. | ||
* | ||
* @event Model#destroying | ||
* @param {Model} model The model firing the event. | ||
* @param {Object} attrs Model firing the event. | ||
* @param {Object} options Options object passed to {@link Model#save save}. | ||
* @returns {Promise} | ||
*/ | ||
return this.triggerThen('destroying', this, options); | ||
}).then(function() { | ||
}).then(function () { | ||
return sync.del(); | ||
}).then(function(resp) { | ||
}).then(function (resp) { | ||
if (options.require && resp === 0) { | ||
@@ -254,2 +1062,15 @@ throw new this.constructor.NoRowsDeletedError('No Rows Deleted'); | ||
this.clear(); | ||
/** | ||
* Destroyed event. | ||
* | ||
* Fired before a `delete` query. A promise may be returned from the event | ||
* handler for async behaviour. | ||
* | ||
* @event Model#destroyed | ||
* @param {Model} model The model firing the event. | ||
* @param {Object} attrs Model firing the event. | ||
* @param {Object} options Options object passed to {@link Model#save save}. | ||
* @returns {Promise} | ||
*/ | ||
return this.triggerThen('destroyed', this, resp, options); | ||
@@ -259,5 +1080,11 @@ }).then(this._reset); | ||
// Reset the query builder, called internally | ||
// each time a query is run. | ||
resetQuery: function() { | ||
/** | ||
* 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 {@link Sync} | ||
* | ||
* @method Model#resetQuery | ||
* @returns {Model} Self, this method is chainable. | ||
*/ | ||
resetQuery: function resetQuery() { | ||
this._knex = null; | ||
@@ -267,22 +1094,96 @@ return this; | ||
// Tap into the "query chain" for this model. | ||
query: function() { | ||
return Helpers.query(this, _.toArray(arguments)); | ||
/** | ||
* 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. If the method argument is a function, it will be | ||
* called with the Knex query builder as the context and the first argument, | ||
* returning the current model. | ||
* | ||
* @example | ||
* | ||
* model | ||
* .query('where', 'other_id', '=', '5') | ||
* .fetch() | ||
* .then(function(model) { | ||
* // ... | ||
* }); | ||
* | ||
* model | ||
* .query({where: {other_id: '5'}, orWhere: {key: 'value'}}) | ||
* .fetch() | ||
* .then(function(model) { | ||
* // ... | ||
* }); | ||
* | ||
* model.query(function(qb) { | ||
* qb.where('other_person', 'LIKE', '%Demo').orWhere('other_id', '>', 10); | ||
* }).fetch() | ||
* .then(function(model) { // ... | ||
* | ||
* let qb = model.query(); | ||
* qb.where({id: 1}).select().then(function(resp) { // ... | ||
* | ||
* @method Model#query | ||
* @param {function|Object|...string=} arguments The query method. | ||
* @returns {Model|QueryBuilder} | ||
* Will return this model or, if called with no arguments, the underlying query builder. | ||
* | ||
* @see {@link http://knexjs.org/#Builder Knex `QueryBuilder`} | ||
*/ | ||
query: function query() { | ||
return _helpers2['default'].query(this, _lodash2['default'].toArray(arguments)); | ||
}, | ||
// Add the most common conditional directly to the model, everything else | ||
// can be accessed with the `query` method. | ||
where: function() { | ||
var args = _.toArray(arguments); | ||
/** | ||
* The where method is used as convenience for the most common {@link | ||
* Model#query query} method, adding a where clause to the builder. Any | ||
* additional knex methods may be accessed using {@link Model#query query}. | ||
* | ||
* Accepts either key, value syntax, or a hash of attributes. | ||
* | ||
* @example | ||
* | ||
* model.where('favorite_color', '<>', 'green').fetch().then(function() { //... | ||
* // or | ||
* model.where('favorite_color', 'red').fetch().then(function() { //... | ||
* // or | ||
* model.where({favorite_color: 'red', shoe_size: 12}).then(function() { //... | ||
* | ||
* @method Model#where | ||
* @param {Object|...string} method | ||
* | ||
* Either `key, [operator], value` syntax, or a hash of attributes to | ||
* match. Note that these must be formatted as they are in the database, | ||
* not how they are stored after {@link Model#parse}. | ||
* | ||
* @returns {Model} Self, this method is chainable. | ||
* | ||
* @see Model#query | ||
*/ | ||
where: function where() { | ||
var args = _lodash2['default'].toArray(arguments); | ||
return this.query.apply(this, ['where'].concat(args)); | ||
}, | ||
// Creates and returns a new `Sync` instance. | ||
sync: function(options) { | ||
return new Sync(this, options); | ||
/** | ||
* Creates and returns a new Bookshelf.Sync instance. | ||
* | ||
* @method Model#sync | ||
* @private | ||
* @returns Sync | ||
*/ | ||
sync: function sync(options) { | ||
return new _sync2['default'](this, options); | ||
}, | ||
// Helper for setting up the `morphOne` or `morphMany` relations. | ||
_morphOneOrMany: function(Target, morphName, columnNames, morphValue, type) { | ||
if (!_.isArray(columnNames)) { | ||
/** | ||
* Helper for setting up the `morphOne` or `morphMany` relations. | ||
* | ||
* @method Model#_morphOneOrMany | ||
* @private | ||
*/ | ||
_morphOneOrMany: function _morphOneOrMany(Target, morphName, columnNames, morphValue, type) { | ||
if (!_lodash2['default'].isArray(columnNames)) { | ||
// Shift by one place | ||
@@ -293,11 +1194,20 @@ morphValue = columnNames; | ||
if (!morphName || !Target) throw new Error('The polymorphic `name` and `Target` are required.'); | ||
return this._relation(type, Target, {morphName: morphName, morphValue: morphValue, columnNames: columnNames}).init(this); | ||
return this._relation(type, Target, { morphName: morphName, morphValue: morphValue, columnNames: columnNames }).init(this); | ||
}, | ||
// Handles the response data for the model, returning from the model's fetch call. | ||
// Todo: {silent: true, parse: true}, for parity with collection#set | ||
// need to check on Backbone's status there, ticket #2636 | ||
_handleResponse: function(response) { | ||
/** | ||
* @name Model#_handleResponse | ||
* @private | ||
* @description | ||
* | ||
* Handles the response data for the model, returning from the model's fetch call. | ||
* | ||
* @param {Object} Response from Knex query. | ||
* | ||
* @todo: need to check on Backbone's status there, ticket #2636 | ||
* @todo: {silent: true, parse: true}, for parity with collection#set | ||
*/ | ||
_handleResponse: function _handleResponse(response) { | ||
var relatedData = this.relatedData; | ||
this.set(this.parse(response[0]), {silent: true})._reset(); | ||
this.set(this.parse(response[0]), { silent: true })._reset(); | ||
if (relatedData && relatedData.isJoined()) { | ||
@@ -308,5 +1218,13 @@ relatedData.parsePivot([this]); | ||
// Handle the related data loading on the model. | ||
_handleEager: function(response, options) { | ||
return new EagerRelation([this], response, this).fetch(options); | ||
/** | ||
* @name Model#_handleEager | ||
* @private | ||
* @description | ||
* | ||
* Handles the related data loading on the model. | ||
* | ||
* @param {Object} Response from Knex query. | ||
*/ | ||
_handleEager: function _handleEager(response, options) { | ||
return new _eager2['default']([this], response, this).fetch(options); | ||
} | ||
@@ -316,6 +1234,30 @@ | ||
extended: function(child) { | ||
child.NotFoundError = createError(this.NotFoundError) | ||
child.NoRowsUpdatedError = createError(this.NoRowsUpdatedError) | ||
child.NoRowsDeletedError = createError(this.NoRowsDeletedError) | ||
extended: function extended(child) { | ||
/** | ||
* @class Model.NotFoundError | ||
* @description | ||
* | ||
* Thrown when no records are found by {@link Model#fetch fetch}, {@link | ||
* Model#fetchAll fetchAll} or {@link Model#refresh} when called with the | ||
* `{require: true}` option. | ||
*/ | ||
child.NotFoundError = (0, _createError2['default'])(this.NotFoundError); | ||
/** | ||
* @class Model.NoRowsUpdated | ||
* @description | ||
* | ||
* Thrown when no records are found by {@link Model#fetch fetch} or | ||
* {@link Model#refresh} unless called with the `{require: false}` option. | ||
*/ | ||
child.NoRowsUpdatedError = (0, _createError2['default'])(this.NoRowsUpdatedError); | ||
/** | ||
* @class Model.NoRowsDeletedError | ||
* @description | ||
* | ||
* Thrown when no record is deleted by {@link Model#destroy destroy} | ||
* if called with the `{require: true}` option. | ||
*/ | ||
child.NoRowsDeletedError = (0, _createError2['default'])(this.NoRowsDeletedError); | ||
} | ||
@@ -325,6 +1267,6 @@ | ||
BookshelfModel.NotFoundError = Errors.NotFoundError, | ||
BookshelfModel.NoRowsUpdatedError = Errors.NoRowsUpdatedError, | ||
BookshelfModel.NoRowsDeletedError = Errors.NoRowsDeletedError | ||
BookshelfModel.NotFoundError = _errors2['default'].NotFoundError; | ||
BookshelfModel.NoRowsUpdatedError = _errors2['default'].NoRowsUpdatedError; | ||
BookshelfModel.NoRowsDeletedError = _errors2['default'].NoRowsDeletedError; | ||
module.exports = BookshelfModel; | ||
module.exports = BookshelfModel; |
// Relation | ||
// --------------- | ||
var _ = require('lodash'); | ||
var inherits = require('inherits'); | ||
'use strict'; | ||
var _ = require('lodash'); | ||
var inflection = require('inflection'); | ||
var Helpers = require('./helpers'); | ||
var ModelBase = require('./base/model'); | ||
var Helpers = require('./helpers'); | ||
var ModelBase = require('./base/model'); | ||
var RelationBase = require('./base/relation'); | ||
var Promise = require('./base/promise'); | ||
var Promise = require('./base/promise'); | ||
var push = [].push; | ||
var push = [].push; | ||
var BookshelfRelation = RelationBase.extend({ | ||
@@ -19,5 +19,5 @@ | ||
// without keeping any hard references. | ||
init: function(parent) { | ||
this.parentId = parent.id; | ||
this.parentTableName = _.result(parent, 'tableName'); | ||
init: function init(parent) { | ||
this.parentId = parent.id; | ||
this.parentTableName = _.result(parent, 'tableName'); | ||
this.parentIdAttribute = _.result(parent, 'idAttribute'); | ||
@@ -34,3 +34,3 @@ | ||
this.target = Helpers.morphCandidate(this.candidates, attributes[this.key('morphKey')]); | ||
this.targetTableName = _.result(this.target.prototype, 'tableName'); | ||
this.targetTableName = _.result(this.target.prototype, 'tableName'); | ||
this.targetIdAttribute = _.result(this.target.prototype, 'idAttribute'); | ||
@@ -44,3 +44,3 @@ } | ||
var target = this.target ? this.relatedInstance() : {}; | ||
target.relatedData = this; | ||
target.relatedData = this; | ||
@@ -56,3 +56,3 @@ if (this.type === 'belongsToMany') { | ||
// which includes any additional keys for the relation. | ||
through: function(source, Target, options) { | ||
through: function through(source, Target, options) { | ||
var type = this.type; | ||
@@ -85,32 +85,42 @@ if (type !== 'hasOne' && type !== 'hasMany' && type !== 'belongsToMany' && type !== 'belongsTo') { | ||
// `foreignKey`, `otherKey`, `throughForeignKey`. | ||
key: function(keyName) { | ||
key: function key(keyName) { | ||
var idKeyName; | ||
if (this[keyName]) return this[keyName]; | ||
if (keyName === 'otherKey') { | ||
return this[keyName] = singularMemo(this.targetTableName) + '_' + this.targetIdAttribute; | ||
switch (keyName) { | ||
case 'otherKey': | ||
this[keyName] = singularMemo(this.targetTableName) + '_' + this.targetIdAttribute; | ||
break; | ||
case 'throughForeignKey': | ||
this[keyName] = singularMemo(this.joinTable()) + '_' + this.throughIdAttribute; | ||
break; | ||
case 'foreignKey': | ||
switch (this.type) { | ||
case 'morphTo': | ||
idKeyName = this.columnNames && this.columnNames[1] ? this.columnNames[1] : this.morphName + '_id'; | ||
this[keyName] = idKeyName; | ||
break; | ||
case 'belongsTo': | ||
this[keyName] = singularMemo(this.targetTableName) + '_' + this.targetIdAttribute; | ||
break; | ||
default: | ||
if (this.isMorph()) { | ||
this[keyName] = this.columnNames && this.columnNames[1] ? this.columnNames[1] : this.morphName + '_id'; | ||
break; | ||
} | ||
this[keyName] = singularMemo(this.parentTableName) + '_' + this.parentIdAttribute; | ||
break; | ||
} | ||
break; | ||
case 'morphKey': | ||
this[keyName] = this.columnNames && this.columnNames[0] ? this.columnNames[0] : this.morphName + '_type'; | ||
break; | ||
case 'morphValue': | ||
this[keyName] = this.parentTableName || this.targetTableName; | ||
break; | ||
} | ||
if (keyName === 'throughForeignKey') { | ||
return this[keyName] = singularMemo(this.joinTable()) + '_' + this.throughIdAttribute; | ||
} | ||
if (keyName === 'foreignKey') { | ||
if (this.type === 'morphTo') { | ||
idKeyName = this.columnNames && this.columnNames[1] ? this.columnNames[1] : this.morphName + '_id'; | ||
return this[keyName] = idKeyName; | ||
} | ||
if (this.type === 'belongsTo') return this[keyName] = singularMemo(this.targetTableName) + '_' + this.targetIdAttribute; | ||
if (this.isMorph()) { | ||
idKeyName = this.columnNames && this.columnNames[1] ? this.columnNames[1] : this.morphName + '_id'; | ||
return this[keyName] = idKeyName; | ||
} | ||
return this[keyName] = singularMemo(this.parentTableName) + '_' + this.parentIdAttribute; | ||
} | ||
if (keyName === 'morphKey') { | ||
var typeKeyName = this.columnNames && this.columnNames[0] ? this.columnNames[0] : this.morphName + '_type'; | ||
return this[keyName] = typeKeyName; | ||
} | ||
if (keyName === 'morphValue') return this[keyName] = this.parentTableName || this.targetTableName; | ||
return this[keyName]; | ||
}, | ||
// Injects the necessary `select` constraints into a `knex` query builder. | ||
selectConstraints: function(knex, options) { | ||
selectConstraints: function selectConstraints(knex, options) { | ||
var resp = options.parentResponse; | ||
@@ -129,3 +139,3 @@ | ||
var currentColumns = _.findWhere(knex._statements, {grouping: 'columns'}); | ||
var currentColumns = _.findWhere(knex._statements, { grouping: 'columns' }); | ||
@@ -147,3 +157,3 @@ if (!currentColumns || currentColumns.length === 0) { | ||
// Inject & validates necessary `through` constraints for the current model. | ||
joinColumns: function(knex) { | ||
joinColumns: function joinColumns(knex) { | ||
var columns = []; | ||
@@ -155,3 +165,3 @@ var joinTable = this.joinTable(); | ||
push.apply(columns, this.pivotColumns); | ||
knex.columns(_.map(columns, function(col) { | ||
knex.columns(_.map(columns, function (col) { | ||
return joinTable + '.' + col + ' as _pivot_' + col; | ||
@@ -162,3 +172,3 @@ })); | ||
// Generates the join clauses necessary for the current relation. | ||
joinClauses: function(knex) { | ||
joinClauses: function joinClauses(knex) { | ||
var joinTable = this.joinTable(); | ||
@@ -168,25 +178,12 @@ | ||
var targetKey = (this.type === 'belongsTo' ? this.key('foreignKey') : this.key('otherKey')); | ||
var targetKey = this.type === 'belongsTo' ? this.key('foreignKey') : this.key('otherKey'); | ||
knex.join( | ||
joinTable, | ||
joinTable + '.' + targetKey, '=', | ||
this.targetTableName + '.' + this.targetIdAttribute | ||
); | ||
knex.join(joinTable, joinTable + '.' + targetKey, '=', this.targetTableName + '.' + this.targetIdAttribute); | ||
// A `belongsTo` -> `through` is currently the only relation with two joins. | ||
if (this.type === 'belongsTo') { | ||
knex.join( | ||
this.parentTableName, | ||
joinTable + '.' + this.throughIdAttribute, '=', | ||
this.parentTableName + '.' + this.key('throughForeignKey') | ||
); | ||
knex.join(this.parentTableName, joinTable + '.' + this.throughIdAttribute, '=', this.parentTableName + '.' + this.key('throughForeignKey')); | ||
} | ||
} else { | ||
knex.join( | ||
joinTable, | ||
joinTable + '.' + this.throughIdAttribute, '=', | ||
this.targetTableName + '.' + this.key('throughForeignKey') | ||
); | ||
knex.join(joinTable, joinTable + '.' + this.throughIdAttribute, '=', this.targetTableName + '.' + this.key('throughForeignKey')); | ||
} | ||
@@ -197,3 +194,3 @@ }, | ||
// passed in when the relation was formed. | ||
whereClauses: function(knex, resp) { | ||
whereClauses: function whereClauses(knex, resp) { | ||
var key; | ||
@@ -205,4 +202,3 @@ | ||
} else { | ||
key = this.targetTableName + '.' + | ||
(this.isInverse() ? this.targetIdAttribute : this.key('foreignKey')); | ||
key = this.targetTableName + '.' + (this.isInverse() ? this.targetIdAttribute : this.key('foreignKey')); | ||
} | ||
@@ -218,3 +214,3 @@ | ||
// Fetches all `eagerKeys` from the current relation. | ||
eagerKeys: function(resp) { | ||
eagerKeys: function eagerKeys(resp) { | ||
var key = this.isInverse() && !this.isThrough() ? this.key('foreignKey') : this.parentIdAttribute; | ||
@@ -225,8 +221,5 @@ return _.uniq(_.pluck(resp, key)); | ||
// Generates the appropriate standard join table. | ||
joinTable: function() { | ||
joinTable: function joinTable() { | ||
if (this.isThrough()) return this.throughTableName; | ||
return this.joinTableName || [ | ||
this.parentTableName, | ||
this.targetTableName | ||
].sort().join('_'); | ||
return this.joinTableName || [this.parentTableName, this.targetTableName].sort().join('_'); | ||
}, | ||
@@ -236,3 +229,3 @@ | ||
// the `relatedData` settings and the models passed in. | ||
relatedInstance: function(models) { | ||
relatedInstance: function relatedInstance(models) { | ||
models = models || []; | ||
@@ -246,3 +239,3 @@ | ||
if (!(Target.prototype instanceof ModelBase)) { | ||
throw new Error('The `'+this.type+'` related object must be a Bookshelf.Model'); | ||
throw new Error('The ' + this.type + ' related object must be a Bookshelf.Model'); | ||
} | ||
@@ -255,7 +248,5 @@ return models[0] || new Target(); | ||
if (Target.prototype instanceof ModelBase) { | ||
Target = this.Collection.extend({ | ||
model: Target | ||
}); | ||
return Target.collection(models, { parse: true }); | ||
} | ||
return new Target(models, {parse: true}); | ||
return new Target(models, { parse: true }); | ||
}, | ||
@@ -265,3 +256,5 @@ | ||
// we're handling, for easy attachment to the parent models. | ||
eagerPair: function(relationName, related, parentModels) { | ||
eagerPair: function eagerPair(relationName, related, parentModels) { | ||
var _this = this; | ||
var model; | ||
@@ -271,5 +264,5 @@ | ||
if (this.type === 'morphTo') { | ||
parentModels = _.filter(parentModels, function(model) { | ||
return model.get(this.key('morphKey')) === this.key('morphValue'); | ||
}, this); | ||
parentModels = _.filter(parentModels, function (m) { | ||
return m.get(_this.key('morphKey')) === _this.key('morphValue'); | ||
}); | ||
} | ||
@@ -281,14 +274,14 @@ | ||
// Group all of the related models for easier association with their parent models. | ||
var grouped = _.groupBy(related, function(model) { | ||
if (model.pivot) { | ||
return this.isInverse() && this.isThrough() ? model.pivot.id : | ||
model.pivot.get(this.key('foreignKey')); | ||
var grouped = _.groupBy(related, function (m) { | ||
if (m.pivot) { | ||
return _this.isInverse() && _this.isThrough() ? m.pivot.id : m.pivot.get(_this.key('foreignKey')); | ||
} else { | ||
return this.isInverse() ? model.id : model.get(this.key('foreignKey')); | ||
return _this.isInverse() ? m.id : m.get(_this.key('foreignKey')); | ||
} | ||
}, this); | ||
}); | ||
// Loop over the `parentModels` and attach the grouped sub-models, | ||
// keeping the `relatedData` on the new related instance. | ||
for (var i = 0, l = parentModels.length; i < l; i++) { | ||
var i = -1; | ||
while (++i < parentModels.length) { | ||
model = parentModels[i]; | ||
@@ -309,3 +302,4 @@ var groupedKey; | ||
// its parsed attributes | ||
for (i = 0, l = related.length; i < l; i++) { | ||
i = -1; | ||
while (++i < related.length) { | ||
model = related[i]; | ||
@@ -321,6 +315,9 @@ model.attributes = model.parse(model.attributes); | ||
// join table and assigning them on the pivot model or object as appropriate. | ||
parsePivot: function(models) { | ||
parsePivot: function parsePivot(models) { | ||
var Through = this.throughTarget; | ||
return _.map(models, function(model) { | ||
var data = {}, keep = {}, attrs = model.attributes, through; | ||
return _.map(models, function (model) { | ||
var data = {}, | ||
keep = {}, | ||
attrs = model.attributes, | ||
through; | ||
if (Through) through = new Through(); | ||
@@ -336,3 +333,3 @@ for (var key in attrs) { | ||
if (!_.isEmpty(data)) { | ||
model.pivot = through ? through.set(data, {silent: true}) : new this.Model(data, { | ||
model.pivot = through ? through.set(data, { silent: true }) : new this.Model(data, { | ||
tableName: this.joinTable() | ||
@@ -346,23 +343,23 @@ }); | ||
// A few predicates to help clarify some of the logic above. | ||
isThrough: function() { | ||
return (this.throughTarget != null); | ||
isThrough: function isThrough() { | ||
return this.throughTarget != null; | ||
}, | ||
isJoined: function() { | ||
return (this.type === 'belongsToMany' || this.isThrough()); | ||
isJoined: function isJoined() { | ||
return this.type === 'belongsToMany' || this.isThrough(); | ||
}, | ||
isMorph: function() { | ||
return (this.type === 'morphOne' || this.type === 'morphMany'); | ||
isMorph: function isMorph() { | ||
return this.type === 'morphOne' || this.type === 'morphMany'; | ||
}, | ||
isSingle: function() { | ||
isSingle: function isSingle() { | ||
var type = this.type; | ||
return (type === 'hasOne' || type === 'belongsTo' || type === 'morphOne' || type === 'morphTo'); | ||
return type === 'hasOne' || type === 'belongsTo' || type === 'morphOne' || type === 'morphTo'; | ||
}, | ||
isInverse: function() { | ||
return (this.type === 'belongsTo' || this.type === 'morphTo'); | ||
isInverse: function isInverse() { | ||
return this.type === 'belongsTo' || this.type === 'morphTo'; | ||
}, | ||
// Sets the `pivotColumns` to be retrieved along with the current model. | ||
withPivot: function(columns) { | ||
withPivot: function withPivot(columns) { | ||
if (!_.isArray(columns)) columns = [columns]; | ||
this.pivotColumns || (this.pivotColumns = []); | ||
this.pivotColumns = this.pivotColumns || []; | ||
push.apply(this.pivotColumns, columns); | ||
@@ -374,12 +371,11 @@ } | ||
// Simple memoization of the singularize call. | ||
var singularMemo = (function() { | ||
var singularMemo = (function () { | ||
var cache = Object.create(null); | ||
return function(arg) { | ||
if (arg in cache) { | ||
return cache[arg]; | ||
} else { | ||
return cache[arg] = inflection.singularize(arg); | ||
return function (arg) { | ||
if (!(arg in cache)) { | ||
cache[arg] = inflection.singularize(arg); | ||
} | ||
return cache[arg]; | ||
}; | ||
}()); | ||
})(); | ||
@@ -394,10 +390,10 @@ // Specific to many-to-many relationships, these methods are mixed | ||
// and attaches the model with a join table entry. | ||
attach: function(ids, options) { | ||
return Promise.bind(this).then(function(){ | ||
attach: function attach(ids, options) { | ||
return Promise.bind(this).then(function () { | ||
return this.triggerThen('attaching', this, ids, options); | ||
}).then(function() { | ||
}).then(function () { | ||
return this._handler('insert', ids, options); | ||
}).then(function(resp) { | ||
}).then(function (resp) { | ||
return this.triggerThen('attached', this, resp, options); | ||
}).then(function() { | ||
}).then(function () { | ||
return this; | ||
@@ -413,8 +409,8 @@ }); | ||
// detach all related associations. | ||
detach: function(ids, options) { | ||
return Promise.bind(this).then(function(){ | ||
detach: function detach(ids, options) { | ||
return Promise.bind(this).then(function () { | ||
return this.triggerThen('detaching', this, ids, options); | ||
}).then(function() { | ||
}).then(function () { | ||
return this._handler('delete', ids, options); | ||
}).then(function(resp) { | ||
}).then(function (resp) { | ||
return this.triggerThen('detached', this, resp, options); | ||
@@ -425,3 +421,3 @@ }); | ||
// Update an existing relation's pivot table entry. | ||
updatePivot: function(data, options) { | ||
updatePivot: function updatePivot(data, options) { | ||
return this._handler('update', data, options); | ||
@@ -434,3 +430,3 @@ }, | ||
// output to the model attributes. | ||
withPivot: function(columns) { | ||
withPivot: function withPivot(columns) { | ||
this.relatedData.withPivot(columns); | ||
@@ -442,5 +438,5 @@ return this; | ||
// the `belongsToMany` or `hasOne` / `hasMany` :through relationship. | ||
_handler: Promise.method(function(method, ids, options) { | ||
_handler: Promise.method(function (method, ids, options) { | ||
var pending = []; | ||
if (ids == void 0) { | ||
if (ids == null) { | ||
if (method === 'insert') return Promise.resolve(this); | ||
@@ -453,3 +449,3 @@ if (method === 'delete') pending.push(this._processPivot(method, null, options)); | ||
} | ||
return Promise.all(pending).return(this); | ||
return Promise.all(pending)['return'](this); | ||
}), | ||
@@ -461,7 +457,7 @@ | ||
// Returns a promise. | ||
_processPivot: Promise.method(function(method, item, options) { | ||
var relatedData = this.relatedData | ||
, args = Array.prototype.slice.call(arguments) | ||
, fks = {} | ||
, data = {}; | ||
_processPivot: Promise.method(function (method, item) { | ||
var relatedData = this.relatedData, | ||
args = Array.prototype.slice.call(arguments), | ||
fks = {}, | ||
data = {}; | ||
@@ -495,3 +491,3 @@ fks[relatedData.key('foreignKey')] = relatedData.parentFk; | ||
// returning a promise. | ||
_processPlainPivot: Promise.method(function(method, item, options, data, fks) { | ||
_processPlainPivot: Promise.method(function (method, item, options, data) { | ||
var relatedData = this.relatedData; | ||
@@ -503,3 +499,3 @@ | ||
if (options && options.query) { | ||
Helpers.query.call(null, {_knex: builder}, [options.query]); | ||
Helpers.query.call(null, { _knex: builder }, [options.query]); | ||
} | ||
@@ -514,6 +510,6 @@ | ||
if (method === 'delete') { | ||
return builder.where(data).del().then(function() { | ||
var model; | ||
return builder.where(data).del().then(function () { | ||
if (!item) return collection.reset(); | ||
if (model = collection.get(data[relatedData.key('otherKey')])) { | ||
var model = collection.get(data[relatedData.key('otherKey')]); | ||
if (model) { | ||
collection.remove(model); | ||
@@ -532,3 +528,3 @@ } | ||
return builder.insert(data).then(function() { | ||
return builder.insert(data).then(function () { | ||
collection.add(item); | ||
@@ -541,15 +537,15 @@ }); | ||
// methods. Returns a promise. | ||
_processModelPivot: Promise.method(function(method, item, options, data, fks) { | ||
var relatedData = this.relatedData | ||
, JoinModel = relatedData.throughTarget | ||
, instance = new JoinModel; | ||
_processModelPivot: Promise.method(function (method, item, options, data, fks) { | ||
var relatedData = this.relatedData, | ||
JoinModel = relatedData.throughTarget, | ||
joinModel = new JoinModel(); | ||
fks = instance.parse(fks); | ||
data = instance.parse(data); | ||
fks = joinModel.parse(fks); | ||
data = joinModel.parse(data); | ||
if (method === 'insert') { | ||
return instance.set(data).save(null, options); | ||
return joinModel.set(data).save(null, options); | ||
} | ||
return instance.set(fks).fetch({ | ||
return joinModel.set(fks).fetch({ | ||
require: true | ||
@@ -560,3 +556,2 @@ }).then(function (instance) { | ||
} | ||
return instance.save(item, options); | ||
@@ -568,2 +563,2 @@ }); | ||
module.exports = BookshelfRelation; | ||
module.exports = BookshelfRelation; |
183
lib/sync.js
// Sync | ||
// --------------- | ||
var _ = require('lodash'); | ||
'use strict'; | ||
var _ = require('lodash'); | ||
var Promise = require('./base/promise'); | ||
@@ -11,5 +13,5 @@ | ||
// part of a transaction, and this information is passed along to `Knex`. | ||
var Sync = function(syncing, options) { | ||
var Sync = function Sync(syncing, options) { | ||
options = options || {}; | ||
this.query = syncing.query(); | ||
this.query = syncing.query(); | ||
this.syncing = syncing.resetQuery(); | ||
@@ -25,3 +27,3 @@ this.options = options; | ||
// current table name | ||
prefixFields: function(fields) { | ||
prefixFields: function prefixFields(fields) { | ||
var tableName = this.syncing.tableName; | ||
@@ -36,9 +38,82 @@ var prefixed = {}; | ||
// Select the first item from the database - only used by models. | ||
first: Promise.method(function() { | ||
this.query.where(this.prefixFields(this.syncing.format( | ||
_.extend(Object.create(null), this.syncing.attributes) | ||
))).limit(1); | ||
first: Promise.method(function (attributes) { | ||
var model = this.syncing, | ||
query = this.query, | ||
whereAttributes, | ||
formatted; | ||
// We'll never use an JSON object for a search, because even | ||
// PostgreSQL, which has JSON type columns, does not support the `=` | ||
// operator. | ||
// | ||
// NOTE: `_.omit` returns an empty object, even if attributes are null. | ||
whereAttributes = _.omit(attributes, _.isPlainObject); | ||
if (!_.isEmpty(whereAttributes)) { | ||
// Format and prefix attributes. | ||
formatted = this.prefixFields(model.format(whereAttributes)); | ||
query.where(formatted); | ||
} | ||
// Limit to a single result. | ||
query.limit(1); | ||
return this.select(); | ||
}), | ||
// Add relational constraints required for either a `count` or `select` query. | ||
constrain: Promise.method(function () { | ||
var knex = this.query, | ||
options = this.options, | ||
relatedData = this.syncing.relatedData, | ||
fks = {}, | ||
through; | ||
// Set the query builder on the options, in-case we need to | ||
// access in the `fetching` event handlers. | ||
options.query = knex; | ||
// Inject all appropriate select costraints dealing with the relation | ||
// into the `knex` query builder for the current instance. | ||
if (relatedData) return Promise['try'](function () { | ||
if (relatedData.isThrough()) { | ||
fks[relatedData.key('foreignKey')] = relatedData.parentFk; | ||
through = new relatedData.throughTarget(fks); | ||
/** | ||
* Fired before a `fetch` operation. A promise may be returned from the | ||
* event handler for async behaviour. | ||
* | ||
* @event Model#fetching | ||
* @param {Model} model The model that has been fetched. | ||
* @param {string[]} columns The columns being retrieved by the query. | ||
* @param {Object} options Options object passed to {@link Model#fetch fetch}. | ||
* @returns {Promise} | ||
*/ | ||
return through.triggerThen('fetching', through, relatedData.pivotColumns, options).then(function () { | ||
relatedData.pivotColumns = through.parse(relatedData.pivotColumns); | ||
}); | ||
} | ||
}); | ||
}), | ||
// Runs a `count` query on the database, adding any necessary relational | ||
// constraints. Returns a promise that resolves to an integer count. | ||
count: Promise.method(function (column) { | ||
var knex = this.query, | ||
options = this.options; | ||
return Promise.bind(this).then(function () { | ||
return this.constrain(); | ||
}).then(function () { | ||
return this.syncing.triggerThen('counting', this.syncing, options); | ||
}).then(function () { | ||
return knex.count((column || '*') + ' as count'); | ||
}).then(function (rows) { | ||
return rows[0].count; | ||
}); | ||
}), | ||
// Runs a `select` query on the database, adding any necessary relational | ||
@@ -49,51 +124,43 @@ // constraints, resetting the query when complete. If there are results and | ||
// options will be called - used by both models & collections. | ||
select: Promise.method(function() { | ||
var knex = this.query | ||
, options = this.options | ||
, relatedData = this.syncing.relatedData | ||
, columnsInQuery = _.some(knex._statements, {grouping:'columns'}) | ||
, columns; | ||
select: Promise.method(function () { | ||
var _this = this; | ||
if (!relatedData) { | ||
columns = options.columns; | ||
// Call the function, if one exists, to constrain the eager loaded query. | ||
if (options._beforeFn) options._beforeFn.call(knex, knex); | ||
if (!_.isArray(columns)) { | ||
columns = columns ? [columns] : | ||
// if columns have been selected in a query closure, use them. | ||
// any user who does this is responsible for prefixing each | ||
// selected column with the correct table name. this will also | ||
// break withRelated queries if the dependent fkey fields are not | ||
// manually included. this is a temporary hack which will be | ||
// replaced by an upcoming rewrite. | ||
columnsInQuery ? [] : [_.result(this.syncing, 'tableName') + '.*']; | ||
} | ||
} | ||
var knex = this.query, | ||
options = this.options, | ||
relatedData = this.syncing.relatedData, | ||
queryContainsColumns, | ||
columns; | ||
// Set the query builder on the options, in-case we need to | ||
// access in the `fetching` event handlers. | ||
options.query = knex; | ||
// Check if any `select` style statements have been called with column | ||
// specifications. This could include `distinct()` with no arguments, which | ||
// does not affect inform the columns returned. | ||
queryContainsColumns = _(knex._statements).where({ grouping: 'columns' }).some('value.length'); | ||
return Promise.bind(this).then(function () { | ||
var fks = {} | ||
, through; | ||
return Promise.resolve(this.constrain()).tap(function () { | ||
// Inject all appropriate select costraints dealing with the relation | ||
// into the `knex` query builder for the current instance. | ||
// If this is a relation, apply the appropriate constraints. | ||
if (relatedData) { | ||
if (relatedData.throughTarget) { | ||
fks[relatedData.key('foreignKey')] = relatedData.parentFk; | ||
through = new relatedData.throughTarget(fks); | ||
return through.triggerThen('fetching', through, relatedData.pivotColumns, options) | ||
.then(function () { | ||
relatedData.pivotColumns = through.parse(relatedData.pivotColumns); | ||
relatedData.selectConstraints(knex, options); | ||
}); | ||
} else { | ||
relatedData.selectConstraints(knex, options); | ||
relatedData.selectConstraints(knex, options); | ||
} else { | ||
// Call the function, if one exists, to constrain the eager loaded query. | ||
if (options._beforeFn) options._beforeFn.call(knex, knex); | ||
if (options.columns) { | ||
// Normalize single column name into array. | ||
columns = _.isArray(options.columns) ? options.columns : [options.columns]; | ||
} else if (!queryContainsColumns) { | ||
// If columns have already been selected via the `query` method | ||
// we will use them. Otherwise, select all columns in this table. | ||
columns = [_.result(_this.syncing, 'tableName') + '.*']; | ||
} | ||
} | ||
// Set the query builder on the options, for access in the `fetching` | ||
// event handlers. | ||
options.query = knex; | ||
return _this.syncing.triggerThen('fetching', _this.syncing, columns, options); | ||
}).then(function () { | ||
return this.syncing.triggerThen('fetching', this.syncing, columns, options); | ||
}).then(function() { | ||
return knex.select(columns); | ||
@@ -104,3 +171,3 @@ }); | ||
// Issues an `insert` command on the query - only used by models. | ||
insert: Promise.method(function() { | ||
insert: Promise.method(function () { | ||
var syncing = this.syncing; | ||
@@ -111,6 +178,7 @@ return this.query.insert(syncing.format(_.extend(Object.create(null), syncing.attributes)), syncing.idAttribute); | ||
// Issues an `update` command on the query - only used by models. | ||
update: Promise.method(function(attrs) { | ||
var syncing = this.syncing, query = this.query; | ||
update: Promise.method(function (attrs) { | ||
var syncing = this.syncing, | ||
query = this.query; | ||
if (syncing.id != null) query.where(syncing.idAttribute, syncing.id); | ||
if (_.where(query._statements, {grouping: 'where'}).length === 0) { | ||
if (_.where(query._statements, { grouping: 'where' }).length === 0) { | ||
throw new Error('A model cannot be updated without a "where" clause or an idAttribute.'); | ||
@@ -122,6 +190,7 @@ } | ||
// Issues a `delete` command on the query. | ||
del: Promise.method(function() { | ||
var query = this.query, syncing = this.syncing; | ||
del: Promise.method(function () { | ||
var query = this.query, | ||
syncing = this.syncing; | ||
if (syncing.id != null) query.where(syncing.idAttribute, syncing.id); | ||
if (_.where(query._statements, {grouping: 'where'}).length === 0) { | ||
if (_.where(query._statements, { grouping: 'where' }).length === 0) { | ||
throw new Error('A model cannot be destroyed without a "where" clause or an idAttribute.'); | ||
@@ -134,2 +203,2 @@ } | ||
module.exports = Sync; | ||
module.exports = Sync; |
{ | ||
"name": "bookshelf", | ||
"version": "0.8.1", | ||
"version": "0.8.2", | ||
"description": "A lightweight ORM for PostgreSQL, MySQL, and SQLite3", | ||
"main": "bookshelf.js", | ||
"scripts": { | ||
"dev": "rm -rf ./lib && mkdir ./lib && babel -w -L -D src/ --out-dir lib/", | ||
"babel": "rm -rf ./lib && mkdir ./lib && babel -L -D src/ --out-dir lib/", | ||
"build": "./scripts/build.sh", | ||
"jshint": "jshint bookshelf.js lib/*", | ||
"test": "mocha -b -t 5000 --check-leaks -R spec test/index.js" | ||
"eslint": "eslint bookshelf.js src/.", | ||
"test": "npm run eslint && npm run babel && mocha -b -t 5000 --check-leaks -R spec test/index.js", | ||
"jsdoc": "rm -rf ./docs/html/* && jsdoc --configure ./scripts/jsdoc.config.json" | ||
}, | ||
@@ -34,5 +37,9 @@ "homepage": "http://bookshelfjs.org", | ||
"devDependencies": { | ||
"babel": "^5.5.8", | ||
"babel-eslint": "^3.1.15", | ||
"chai": "~1.9.1", | ||
"jshint": "~2.7.0", | ||
"eslint": "^0.23.0", | ||
"jsdoc": "^3.3.1", | ||
"knex": "^0.8.0", | ||
"minami": "git+https://github.com/rhys-vdw/minami", | ||
"minimist": "^1.1.0", | ||
@@ -39,0 +46,0 @@ "mocha": "^2.0.1", |
@@ -7,2 +7,3 @@ // Virtuals Plugin | ||
var _ = require('lodash'); | ||
var Promise = require('bluebird'); | ||
var proto = Bookshelf.Model.prototype; | ||
@@ -59,13 +60,83 @@ | ||
// Allow virtuals to be set like normal properties | ||
set: function(key, val, options) { | ||
set: function(key, value, options) { | ||
if (key == null) { | ||
return this; | ||
} | ||
// Determine whether we're in the middle of a patch operation based on the | ||
// existance of the `patchAttributes` object. | ||
var isPatching = this.patchAttributes != null; | ||
// Handle `{key: value}` style arguments. | ||
if (_.isObject(key)) { | ||
return proto.set.call(this, _.omit(key, setVirtual, this), val, options); | ||
var nonVirtuals = _.omit(key, setVirtual, this); | ||
if (isPatching) { | ||
_.extend(this.patchAttributes, nonVirtuals); | ||
} | ||
// Set the non-virtual attributes as normal. | ||
return proto.set.call(this, nonVirtuals, value, options); | ||
} | ||
if (setVirtual.call(this, val, key)) { | ||
// Handle `"key", value` style arguments. | ||
if (setVirtual.call(this, value, key)) { | ||
if (isPatching) { | ||
this.patchAttributes[key] = value; | ||
} | ||
return this; | ||
} | ||
return proto.set.apply(this, arguments); | ||
}, | ||
// Override `save` to keep track of state while doing a `patch` operation. | ||
save: function(key, value, options) { | ||
var attrs; | ||
// Handle both `"key", value` and `{key: value}` -style arguments. | ||
if (key == null || typeof key === "object") { | ||
attrs = key && _.clone(key) || {}; | ||
options = _.clone(value) || {}; | ||
} else { | ||
(attrs = {})[key] = value; | ||
options = options ? _.clone(options) : {}; | ||
} | ||
// Determine whether which kind of save we will do, update or insert. | ||
var method = options.method = this.saveMethod(options); | ||
// Check if we're going to do a patch, in which case deal with virtuals now. | ||
if (options.method === 'update' && options.patch) { | ||
// Extend the model state to collect side effects from the virtual setter | ||
// callback. If `set` is called, this object will be updated in addition | ||
// to the normal `attributes` object. | ||
this.patchAttributes = {} | ||
// Any setter could throw. We need to reject `save` if they do. | ||
try { | ||
// Check if any of the patch attribues are virtuals. If so call their | ||
// setter. Any setter that calls `this.set` will be modifying | ||
// `this.patchAttributes` instead of `this.attributes`. | ||
_.each(attrs, (function (value, key) { | ||
if (setVirtual.call(this, value, key)) { | ||
// This was a virtual, so remove it from the attributes to be | ||
// passed to `Model.save`. | ||
delete attrs[key]; | ||
} | ||
}).bind(this)); | ||
// Now add any changes that occurred during the update. | ||
_.extend(attrs, this.patchAttributes); | ||
} catch (e) { | ||
return Promise.reject(e); | ||
} finally { | ||
// Delete the temporary object. | ||
delete this.patchAttributes; | ||
} | ||
} | ||
return proto.save.call(this, attrs, options); | ||
} | ||
@@ -115,2 +186,2 @@ }); | ||
Bookshelf.Model = Model; | ||
}; | ||
}; |
114
README.md
# [bookshelf.js](http://bookshelfjs.org) [![Build Status](https://travis-ci.org/tgriesser/bookshelf.svg?branch=master)](https://travis-ci.org/tgriesser/bookshelf) | ||
Bookshelf is a Node.js ORM with support for PostgreSQL, MySQL / MariaDB, and SQLite3. | ||
Bookshelf is a JavaScript ORM for Node.js, built on the [Knex](http://knexjs.org) SQL query builder. Featuring both promise based and traditional callback interfaces, it follows the Model & Collection patterns seen in [Backbone.js](http://backbonejs.com), providing transaction support, eager/nested-eager relation loading, polymorphic associations, and support for one-to-one, one-to-many, and many-to-many relations. | ||
It is built atop the <a href="http://knexjs.org">Knex query builder</a>, | ||
and is strongly influenced by the Model and Collection foundations of Backbone.js. | ||
It is designed to work well with PostgreSQL, MySQL, and SQLite3. | ||
It features [transaction support](http://bookshelfjs.org/#Bookshelf-transaction), one-to-one, one-to-many, many-to-many, and polymorphic relations. | ||
The project is [hosted on GitHub](http://github.com/tgriesser/bookshelf/), and has a comprehensive [test suite](https://travis-ci.org/tgriesser/bookshelf). | ||
For documentation, FAQs, and other information, see: http://bookshelfjs.org. | ||
## Introduction | ||
To suggest a feature, report a bug, or for general discussion: http://github.com/tgriesser/bookshelf/issues/ | ||
Bookshelf aims to provide a simple library for common tasks when querying databases in JavaScript, and forming relations between these objects, taking a lot of ideas from the the [Data Mapper Pattern](http://en.wikipedia.org/wiki/Data_mapper_pattern). With a concise, literate codebase, Bookshelf is simple to read, understand, and extend. It doesn't force you to use any specific validation scheme, provides flexible and efficient relation/nested-relation loading, and first class transaction support. It's a lean Object Relational Mapper, allowing you to drop down to the raw knex interface whenever you need a custom query that doesn't quite fit with the stock conventions. | ||
Bookshelf follows 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 | ||
You'll need to install a copy of [knex.js](http://knexjs.org/), and either mysql, pg, or sqlite3 from npm. | ||
```js | ||
$ npm install knex --save | ||
$ npm install bookshelf --save | ||
# Then add one of the following: | ||
$ npm install pg | ||
$ npm install mysql | ||
$ npm install mariasql | ||
$ npm install sqlite3 | ||
``` | ||
The Bookshelf library is initialized by passing an initialized [Knex](http://knexjs.org/) client instance. The [knex documentation](http://knexjs.org/) provides a number of examples for different databases. | ||
```js | ||
var knex = require('knex')({ | ||
client: 'mysql', | ||
connection: { | ||
host : '127.0.0.1', | ||
user : 'your_database_user', | ||
password : 'your_database_password', | ||
database : 'myapp_test', | ||
charset : 'utf8' | ||
} | ||
}); | ||
var bookshelf = require('bookshelf')(knex); | ||
var User = bookshelf.Model.extend({ | ||
tableName: 'users' | ||
}); | ||
``` | ||
This initialization should likely only ever happen once in your application. As it creates a connection pool for the current database, you should use the `bookshelf` instance returned throughout your library. You'll need to store this instance created by the initialize somewhere in the application so you can reference it. A common pattern to follow is to initialize the client in a module so you can easily reference it later: | ||
```js | ||
// In a file named something like bookshelf.js | ||
var knex = require('knex')(dbConfig); | ||
module.exports require('bookshelf')(knex); | ||
// elsewhere, to use the bookshelf client: | ||
var bookshelf = require('./bookshelf'); | ||
var Post = bookshelf.Model.extend({ | ||
// ... | ||
}); | ||
``` | ||
## Examples | ||
We have several examples [on the website](http://bookshelfjs.org). Here is the first one to get you started: | ||
Here is an example to get you started: | ||
@@ -51,4 +103,48 @@ ```js | ||
## Contributing | ||
## Plugins | ||
To contribute to Bookshelf, read the [contribution documentation](CONTRIBUTING.md) for more information. | ||
- [Registry](https://github.com/tgriesser/bookshelf/wiki/Plugin:-Model-Registry): Register models in a central location so that you can refer to them using a string in relations instead of having to require it every time. Helps deal with the challenges of circular module dependencies in Node. | ||
- [Virtuals](https://github.com/tgriesser/bookshelf/wiki/Plugin:-Virtuals): Define virtual properties on your model to compute new values. | ||
- [Visibility](https://github.com/tgriesser/bookshelf/wiki/Plugin:-Visibility): Specify a whitelist/blacklist of model attributes when serialized toJSON. | ||
## Support | ||
Have questions about the library? Come join us in the [#bookshelf freenode IRC channel](http://webchat.freenode.net/?channels=bookshelf) for support on [knex.js](http://knexjs.org/) and bookshelf.js, or post an issue on [Stack Overflow](http://stackoverflow.com/questions/tagged/bookshelf.js) or in the GitHub [issue tracker](https://github.com/tgriesser/bookshelf/issues). | ||
## F.A.Q. | ||
### Can I use standard node.js style callbacks? | ||
Yes - you can call `.asCallback(function(err, resp) {` on any "sync" method and use the standard `(err, result)` style callback interface if you prefer. | ||
### My relations don't seem to be loading, what's up? | ||
Make sure you check that the type is correct for the initial parameters passed to the initial model being fetched. For example `new Model({id: '1'}).load([relations...])` will not return the same as `Model({id: 1}).load([relations...])` - notice that the id is a string in one case and a number in the other. This can be a common mistake if retrieving the id from a url parameter. | ||
This is only an issue if you're eager loading data with load without first fetching the original model. `Model({id: '1'}).fetch({withRelated: [relations...]})` should work just fine. | ||
### My process won't exit after my script is finished, why? | ||
The issue here is that Knex, the database abstraction layer used by Bookshelf, uses connection pooling and thus keeps the database connection open. If you want your process to exit after your script has finished, you will have to call `.destroy(cb)` on the `knex` property of your `Bookshelf` instance or on the `Knex` instance passed during initialization. More information about connection pooling can be found over at the [Knex docs](http://knexjs.org/#Installation-pooling). | ||
### How do I debug? | ||
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](https://github.com/dannycoates/node-inspector), which allows you to debug code with `debugger` statements like you would in the browser. | ||
Bookshelf uses its own copy of the "bluebird" promise library, you can read up here for more on debugging these promises... but in short, adding: | ||
process.stderr.on('data', function(data) { | ||
console.log(data); | ||
}); | ||
At the start of your application code will catch any errors not otherwise caught in the normal promise chain handlers, which is very helpful in debugging. | ||
### How do I run the test suite? | ||
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/bookshelf_config.js'`, replacing with the path to your config file, and the config file is valid, the test suite should run with npm test. | ||
Also note that you will have to create the appropriate database(s) for the test suite to run. For example, with MySQL, you'll need to run the command `create database bookshelf_test;` in addition to exporting the correct test settings prior to running the test suite. | ||
### Can I use Bookshelf outside of Node.js? | ||
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](http://knexjs.org/#Adapters). |
@@ -33,4 +33,9 @@ var _ = require('lodash'); | ||
_.each([MySQL, PostgreSQL, SQLite3], function(bookshelf) { | ||
describe('Dialect: ' + bookshelf.knex.client.dialect, function() { | ||
var dialect = bookshelf.knex.client.dialect; | ||
describe('Dialect: ' + dialect, function() { | ||
this.dialect = dialect; | ||
before(function() { | ||
@@ -42,9 +47,9 @@ return require('./integration/helpers/migration')(bookshelf).then(function() { | ||
this.dialect = bookshelf.knex.client.dialect; | ||
// Only testing this against mysql for now, just so the toString is reliable... | ||
if (bookshelf.knex.client.dialect === 'mysql') { | ||
if (dialect === 'mysql') { | ||
require('./integration/relation')(bookshelf); | ||
} else if (dialect === 'postgresql') { | ||
require('./integration/json')(bookshelf); | ||
} | ||
require('./integration/model')(bookshelf); | ||
@@ -51,0 +56,0 @@ require('./integration/collection')(bookshelf); |
var Promise = global.testPromise; | ||
var assert = require('assert') | ||
var assert = require('assert'); | ||
var _ = require('lodash'); | ||
var equal = assert.equal | ||
var deepEqual = assert.deepEqual | ||
@@ -16,2 +16,12 @@ module.exports = function(bookshelf) { | ||
}; | ||
var checkCount = function(ctx) { | ||
var formatNumber = { | ||
mysql: _.identity, | ||
sqlite3: _.identity, | ||
postgresql: function(count) { return count.toString() } | ||
}[dialect]; | ||
return function(actual, expected) { | ||
expect(actual, formatNumber(expected)); | ||
} | ||
}; | ||
var checkTest = function(ctx) { | ||
@@ -62,2 +72,53 @@ return function(resp) { | ||
describe('count', function() { | ||
it ('counts the number of models in a collection', function() { | ||
return bookshelf.Collection.extend({tableName: 'posts'}) | ||
.forge() | ||
.count() | ||
.then(function(count) { | ||
checkCount(count, 5); | ||
}); | ||
}); | ||
it ('optionally counts by column (excluding null values)', function() { | ||
var authors = bookshelf.Collection.extend({tableName: 'authors'}).forge(); | ||
return authors.count() | ||
.then(function(count) { | ||
checkCount(count, 5); | ||
return authors.count('last_name'); | ||
}).then(function(count) { | ||
checkCount(count, 4); | ||
}); | ||
}); | ||
it ('counts a filtered query', function() { | ||
return bookshelf.Collection.extend({tableName: 'posts'}) | ||
.forge() | ||
.query('where', 'blog_id', 1) | ||
.count() | ||
.then(function(count) { | ||
checkCount(count, 2); | ||
}); | ||
}); | ||
it ('counts a `hasMany` relation', function() { | ||
return new Blog({id: 1}) | ||
.posts() | ||
.count() | ||
.tap(function(count) { | ||
checkCount(count, 2); | ||
}); | ||
}); | ||
it ('counts a `hasMany` `through` relation', function() { | ||
return new Blog({id: 1}) | ||
.comments() | ||
.count() | ||
.tap(function(count) { | ||
checkCount(count, 1); | ||
}); | ||
}); | ||
}); | ||
describe('fetchOne', function() { | ||
@@ -162,2 +223,13 @@ | ||
it('should not set incorrect foreign key in a `hasMany` `through` relation - #768', function() { | ||
// This will fail if an unknown field (eg. `blog_id`) is added to insert query. | ||
return new Blog({id: 768}) | ||
.comments() | ||
.create({post_id: 5, comment: 'test comment'}) | ||
.tap(function (comment) { | ||
return comment.destroy(); | ||
}); | ||
}); | ||
it('should automatically create a join model when joining a belongsToMany', function() { | ||
@@ -214,2 +286,12 @@ | ||
it('correctly parses added relation keys', function() { | ||
return Site.forge({id: 1}).related('authorsParsed') | ||
.create({first_name_parsed: 'John', last_name_parsed: 'Smith'}) | ||
.then(function (author) { | ||
expect(author.get('first_name_parsed')).to.equal('John'); | ||
expect(author.get('last_name_parsed')).to.equal('Smith'); | ||
expect(author.get('site_id_parsed')).to.equal(1); | ||
return author.destroy(); | ||
}); | ||
}); | ||
}); | ||
@@ -216,0 +298,0 @@ |
@@ -76,2 +76,6 @@ var Promise = global.testPromise; | ||
last_name: 'Burgundy' | ||
},{ | ||
site_id: 99, | ||
first_name: 'Anonymous', | ||
last_name: null | ||
}]), | ||
@@ -78,0 +82,0 @@ |
@@ -50,2 +50,5 @@ // All Models & Collections Used in the Tests | ||
}, | ||
authorsParsed: function() { | ||
return this.hasMany(AuthorParsed); | ||
}, | ||
photos: function() { | ||
@@ -146,8 +149,6 @@ return this.morphMany(Photo, 'imageable'); | ||
defaults: { | ||
author: '', | ||
title: '', | ||
body: '', | ||
published: false | ||
name: '', | ||
content: '' | ||
}, | ||
hasTimestamps: true, | ||
hasTimestamps: false, | ||
blog: function() { | ||
@@ -180,3 +181,3 @@ return this.belongsTo(Blog); | ||
email: '', | ||
post: '' | ||
comment: '' | ||
}, | ||
@@ -183,0 +184,0 @@ posts: function() { |
@@ -24,2 +24,14 @@ var _ = require('lodash'); | ||
var checkCount = function(ctx) { | ||
var dialect = bookshelf.knex.client.dialect; | ||
var formatNumber = { | ||
mysql: _.identity, | ||
sqlite3: _.identity, | ||
postgresql: function(count) { return count.toString() } | ||
}[dialect]; | ||
return function(actual, expected) { | ||
expect(actual, formatNumber(expected)); | ||
} | ||
}; | ||
describe('extend/constructor/initialize', function() { | ||
@@ -294,5 +306,23 @@ | ||
describe('refresh', function() { | ||
var Site = Models.Site; | ||
it('will fetch a record by present attributes without an ID attribute', function() { | ||
Site.forge({name: 'knexjs.org'}).refresh().then(function (model) { | ||
expect(model.id).to.equal(1); | ||
}); | ||
}); | ||
it("will update a model's attributes by fetching only by `idAttribute`", function() { | ||
Site.forge({id: 1, name: 'NOT THE CORRECT NAME'}).refresh().then(function (model) { | ||
expect(model.get('name')).to.equal('knexjs.org'); | ||
}); | ||
}); | ||
}); | ||
describe('fetch', function() { | ||
var Site = Models.Site; | ||
var Author = Models.Author; | ||
@@ -340,2 +370,28 @@ it('issues a first (get one) to Knex, triggering a fetched event, returning a promise', function() { | ||
it('allows specification of select columns as an `options` argument', function () { | ||
var model = new Author({id: 1}).fetch({columns: ['first_name']}) | ||
.then(function (model) { | ||
deepEqual(model.toJSON(), {id: 1, first_name: 'Tim'}); | ||
}); | ||
}); | ||
it('allows specification of select columns in query callback', function () { | ||
var model = new Author({id: 1}).query('select','first_name').fetch() | ||
.then(function (model) { | ||
deepEqual(model.toJSON(), {id: 1, first_name: 'Tim'}); | ||
}); | ||
}); | ||
it('will still select default columns if `distinct` is called without columns - #807', function () { | ||
var model = new Author({id: 1}).query('distinct').fetch() | ||
.then(function (model) { | ||
deepEqual(model.toJSON(), { | ||
id: 1, | ||
first_name: 'Tim', | ||
last_name: 'Griesser', | ||
site_id: 1 | ||
}); | ||
}); | ||
}); | ||
}); | ||
@@ -575,2 +631,45 @@ | ||
describe('count', function() { | ||
it('counts the number of models in a collection', function() { | ||
return Models.Post | ||
.forge() | ||
.count() | ||
.then(function(count) { | ||
checkCount(count, 5); | ||
}); | ||
}); | ||
it('optionally counts by column (excluding null values)', function() { | ||
var author = Models.Author.forge(); | ||
return author.count() | ||
.then(function(count) { | ||
checkCount(count, 5); | ||
return author.count('last_name'); | ||
}).then(function(count) { | ||
checkCount(count, 4); | ||
}); | ||
}); | ||
it('counts a filtered query', function() { | ||
return Models.Post | ||
.forge() | ||
.query('where', 'blog_id', 1) | ||
.count() | ||
.then(function(count) { | ||
checkCount(count, 2); | ||
}); | ||
}); | ||
it('resets query after completing', function() { | ||
var posts = Models.Post.collection(); | ||
posts.query('where', 'blog_id', 2).count() | ||
.then(function(count) { | ||
checkCount(count, 2); | ||
return posts.count(); | ||
}) | ||
.then(function(count) { checkCount(count, 2); }); | ||
}); | ||
}); | ||
describe('resetQuery', function() { | ||
@@ -681,11 +780,28 @@ | ||
it('will set the `updated_at` attribute to a date, and the `created_at` for new entries', function() { | ||
var m = new bookshelf.Model(); | ||
var m1 = new bookshelf.Model({id: 1}); | ||
var ts = m.timestamp(); | ||
var ts2 = m1.timestamp(); | ||
equal(_.isDate(ts.created_at), true); | ||
equal(_.isDate(ts.updated_at), true); | ||
equal(_.isEmpty(ts2.created_at), true); | ||
equal(_.isDate(ts2.updated_at), true); | ||
var newModel = new bookshelf.Model({}, {hasTimestamps: true}); | ||
var existingModel = new bookshelf.Model({id: 1}, {hasTimestamps: true}); | ||
newModel.timestamp(); | ||
existingModel.timestamp(); | ||
expect(newModel.get('created_at')).to.be.an.instanceOf(Date); | ||
expect(newModel.get('updated_at')).to.be.an.instanceOf(Date); | ||
expect(existingModel.get('created_at')).to.not.exist; | ||
expect(existingModel.get('updated_at')).to.be.an.instanceOf(Date); | ||
}); | ||
it('will set the `created_at` when inserting new entries', function() { | ||
var model = new bookshelf.Model({id: 1}, {hasTimestamps: true}); | ||
model.timestamp({method: 'insert'}); | ||
expect(model.get('created_at')).to.be.an.instanceOf(Date); | ||
expect(model.get('updated_at')).to.be.an.instanceOf(Date); | ||
}); | ||
it('will not set timestamps on a model without `setTimestamps` set to true', function () { | ||
var model = new bookshelf.Model(); | ||
model.timestamp(); | ||
expect(model.get('created_at')).to.not.exist; | ||
expect(model.get('updated_at')).to.not.exist; | ||
}); | ||
}); | ||
@@ -795,2 +911,10 @@ | ||
describe('Model.count', function() { | ||
it('counts the number of matching records in the database', function() { | ||
return Models.Post.count().then(function(count) { | ||
checkCount(count, 5); | ||
}); | ||
}); | ||
}); | ||
describe('model.once', function() { | ||
@@ -821,4 +945,16 @@ | ||
describe('model.clone', function() { | ||
var Post = Models.Post; | ||
it('should be equivalent when cloned', function() { | ||
var original = Post.forge({author: 'Johnny', body: 'body'}); | ||
original.related('comments').add({email: 'some@email.com'}); | ||
var cloned = original.clone(); | ||
deepEqual(_.omit(cloned, 'cid'), _.omit(original, 'cid')); | ||
}); | ||
}); | ||
}); | ||
}; |
@@ -14,2 +14,7 @@ var _ = require('lodash'); | ||
var Models = require('./helpers/objects')(Bookshelf).Models; | ||
var Site = Models.Site; | ||
var Author = Models.Author; | ||
it('can be the name of an included plugin', function () { | ||
@@ -40,4 +45,20 @@ Bookshelf.plugin('registry'); | ||
it('can modify the `Collection` model returned by `Model#collection`', function () { | ||
var testPlugin = function (bookshelf, options) { | ||
bookshelf.Collection = bookshelf.Collection.extend({ | ||
test: 'test' | ||
}); | ||
} | ||
Bookshelf.plugin(testPlugin); | ||
expect(Bookshelf.Model.collection().test).to.equal('test'); | ||
}); | ||
it('can modify the `Collection` model used by relations', function () { | ||
var authors = Site.forge().related('authors'); | ||
expect(authors.test).to.equal('test'); | ||
}); | ||
}); | ||
}; |
@@ -208,3 +208,53 @@ var _ = require('lodash'); | ||
it('behaves correctly during a `patch` save - #542', function() { | ||
var Model = bookshelf.Model.extend({ | ||
tableName: 'authors', | ||
virtuals: { | ||
full_name: { | ||
set: function(fullName) { | ||
var names = fullName.split(' '); | ||
return this.set({ | ||
first_name: names[0], | ||
last_name: names[1] | ||
}); | ||
}, | ||
get: function() { | ||
return [this.get('first_name'), this.get('last_name')].join(' '); | ||
} | ||
} | ||
} | ||
}); | ||
return new Model({site_id: 5}).save() | ||
.then(function(model) { | ||
return model.save({site_id: 2, full_name: 'Oderus Urungus'}, {patch: true}) | ||
}).tap(function(result) { | ||
expect(result.get('site_id')).to.equal(2); | ||
expect(result.get('first_name')).to.equal('Oderus'); | ||
expect(result.get('last_name')).to.equal('Urungus'); | ||
return result.destroy(); | ||
}); | ||
}); | ||
it('save should be rejected after `set` throws an exception during a `patch` operation.', function() { | ||
var Model = bookshelf.Model.extend({ | ||
tableName: 'authors', | ||
virtuals: { | ||
will_cause_error: { | ||
set: function(fullName) { | ||
throw new Error('Deliberately failing'); | ||
}, | ||
get: _.noop | ||
} | ||
} | ||
}); | ||
return Model.forge({id: 4, first_name: 'Ned'}) | ||
.save({will_cause_error: 'value'}, {patch: true}) | ||
.catch(function(error) { | ||
expect(error.message).to.equal('Deliberately failing'); | ||
}) | ||
}); | ||
}); | ||
}; |
@@ -23,2 +23,5 @@ var Promise = testPromise; | ||
tableName: 'testtable', | ||
isNew: function() { | ||
return true | ||
}, | ||
queryData: qd, | ||
@@ -57,2 +60,6 @@ query: function() { | ||
var attributes = { | ||
'Some': 'column', | ||
'Another': 'column' | ||
}; | ||
var sync = new Sync(_.extend(stubSync(), { | ||
@@ -65,6 +72,2 @@ format: function(attrs) { | ||
return data; | ||
}, | ||
attributes: { | ||
'Some': 'column', | ||
'Another': 'column' | ||
} | ||
@@ -78,3 +81,3 @@ })); | ||
}; | ||
return sync.first(); | ||
return sync.first(attributes); | ||
@@ -81,0 +84,0 @@ }); |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
841985
86
22892
150
15
16
1