Comparing version 0.7.9 to 0.8.0
159
bookshelf.js
@@ -1,2 +0,2 @@ | ||
// Bookshelf.js 0.7.9 | ||
// Bookshelf.js 0.8.0 | ||
// --------------- | ||
@@ -8,63 +8,65 @@ | ||
// http://bookshelfjs.org | ||
var _ = require('lodash'); | ||
var inherits = require('inherits'); | ||
var semver = require('semver'); | ||
var helpers = require('./lib/helpers') | ||
var Bookshelf = function() { | ||
return Bookshelf.initialize.apply(null, arguments); | ||
}; | ||
// We've supplemented `Events` with a `triggerThen` | ||
// method to allow for asynchronous event handling via promises. We also | ||
// mix this into the prototypes of the main objects in the library. | ||
var Events = require('./lib/base/events'); | ||
// Constructor for a new `Bookshelf` object, it accepts | ||
// an active `knex` instance and initializes the appropriate | ||
// `Model` and `Collection` constructors for use in the current instance. | ||
Bookshelf.initialize = function(knex) { | ||
// All core modules required for the bookshelf instance. | ||
var BookshelfModel = require('./lib/model'); | ||
var BookshelfCollection = require('./lib/collection'); | ||
var BookshelfRelation = require('./lib/relation'); | ||
var Errors = require('./lib/errors'); | ||
function Bookshelf(knex) { | ||
var bookshelf = { | ||
VERSION: '0.7.9' | ||
VERSION: '0.8.0' | ||
}; | ||
var _ = require('lodash'); | ||
var inherits = require('inherits'); | ||
var semver = require('semver'); | ||
var range = '>=0.6.10 <0.9.0'; | ||
if (!semver.satisfies(knex.VERSION, range)) { | ||
throw new Error('The knex version is ' + knex.VERSION + ' which does not satisfy the Bookshelf\'s requirement ' + range); | ||
} | ||
// We've supplemented `Events` with a `triggerThen` | ||
// method to allow for asynchronous event handling via promises. We also | ||
// mix this into the prototypes of the main objects in the library. | ||
var Events = require('./lib/base/events'); | ||
var Model = bookshelf.Model = BookshelfModel.extend({ | ||
_builder: builderFn, | ||
// All core modules required for the bookshelf instance. | ||
var BookshelfModel = require('./lib/model'); | ||
var BookshelfCollection = require('./lib/collection'); | ||
var BookshelfRelation = require('./lib/relation'); | ||
var Errors = require('./lib/errors'); | ||
// The `Model` constructor is referenced as a property on the `Bookshelf` instance, | ||
// mixing in the correct `builder` method, as well as the `relation` method, | ||
// passing in the correct `Model` & `Collection` constructors for later reference. | ||
_relation: function(type, Target, options) { | ||
if (type !== 'morphTo' && !_.isFunction(Target)) { | ||
throw new Error('A valid target model must be defined for the ' + | ||
_.result(this, 'tableName') + ' ' + type + ' relation'); | ||
} | ||
return new Relation(type, Target, options); | ||
} | ||
// If the knex isn't a `Knex` instance, we'll assume it's | ||
// a compatible config object and pass it through to create a new instance. | ||
// This behavior is now deprecated. | ||
if (_.isPlainObject(knex)) { | ||
console.warn('Initializing Bookshelf with a config object is deprecated, please pass an initialized knex.js instance.'); | ||
knex = require('knex')(knex); | ||
} | ||
}, { | ||
var range = '>=0.6.10'; | ||
if (!semver.satisfies(knex.VERSION, range)) { | ||
throw new Error('The knex version is ' + knex.VERSION + ' which does not satisfy the Bookshelf\'s requirement ' + range); | ||
} | ||
forge: forge, | ||
var Model = bookshelf.Model = function() { | ||
BookshelfModel.apply(this, arguments); | ||
}; | ||
var Collection = bookshelf.Collection = function() { | ||
BookshelfCollection.apply(this, arguments); | ||
}; | ||
inherits(Model, BookshelfModel); | ||
inherits(Collection, BookshelfCollection); | ||
collection: function(rows, options) { | ||
return new Collection((rows || []), _.extend({}, options, {model: this})); | ||
}, | ||
_.extend(Model, BookshelfModel); | ||
_.extend(Collection, BookshelfCollection); | ||
fetchAll: function(options) { | ||
return this.forge().fetchAll(options); | ||
} | ||
}) | ||
Model.prototype._builder = | ||
Collection.prototype._builder = function(tableName) { | ||
var builder = knex(tableName); | ||
var instance = this; | ||
return builder.on('query', function(data) { | ||
instance.trigger('query', data); | ||
}); | ||
}; | ||
var Collection = bookshelf.Collection = BookshelfCollection.extend({ | ||
_builder: builderFn | ||
}, { | ||
forge: forge | ||
}); | ||
@@ -76,25 +78,7 @@ // The collection also references the correct `Model`, specified above, for creating | ||
function Relation() { | ||
BookshelfRelation.apply(this, arguments); | ||
} | ||
inherits(Relation, BookshelfRelation); | ||
Relation.prototype.Model = Model; | ||
Relation.prototype.Collection = Collection; | ||
var Relation = BookshelfRelation.extend({ | ||
Model: Model, | ||
Collection: Collection | ||
}) | ||
// The `Model` constructor is referenced as a property on the `Bookshelf` instance, | ||
// mixing in the correct `builder` method, as well as the `relation` method, | ||
// passing in the correct `Model` & `Collection` constructors for later reference. | ||
Model.prototype._relation = function(type, Target, options) { | ||
if (type !== 'morphTo' && !_.isFunction(Target)) { | ||
throw new Error('A valid target model must be defined for the ' + | ||
_.result(this, 'tableName') + ' ' + type + ' relation'); | ||
} | ||
return new Relation(type, Target, options); | ||
}; | ||
// Shortcut for creating a new collection with the current collection. | ||
Model.collection = function(rows, options) { | ||
return new Collection((rows || []), _.extend({}, options, {model: this})); | ||
}; | ||
// A `Bookshelf` instance may be used as a top-level pub-sub bus, as it mixes in the | ||
@@ -117,3 +101,5 @@ // `Events` object. It also contains the version number, and a `Transaction` method | ||
} catch (e) { | ||
require(plugin)(this, options); | ||
if (!process.browser) { | ||
require(plugin)(this, options) | ||
} | ||
} | ||
@@ -139,8 +125,16 @@ } else if (_.isArray(plugin)) { | ||
// and more chainable. | ||
Model.forge = Collection.forge = function() { | ||
function forge() { | ||
var inst = Object.create(this.prototype); | ||
var obj = this.apply(inst, arguments); | ||
return (Object(obj) === obj ? obj : inst); | ||
}; | ||
} | ||
function builderFn(tableName) { | ||
var builder = knex(tableName); | ||
var instance = this; | ||
return builder.on('query', function(data) { | ||
instance.trigger('query', data); | ||
}); | ||
} | ||
// Attach `where`, `query`, and `fetchAll` as static methods. | ||
@@ -154,10 +148,15 @@ ['where', 'query'].forEach(function(method) { | ||
}); | ||
Model.fetchAll = function(options) { return this.forge().fetchAll(options); }; | ||
return bookshelf; | ||
} | ||
Model.extend = Collection.extend = require('simple-extend'); | ||
return bookshelf; | ||
// Constructor for a new `Bookshelf` object, it accepts | ||
// an active `knex` instance and initializes the appropriate | ||
// `Model` and `Collection` constructors for use in the current instance. | ||
Bookshelf.initialize = function(knex) { | ||
helpers.warn("Bookshelf.initialize is deprecated, pass knex directly: require('bookshelf')(knex)") | ||
return new Bookshelf(knex) | ||
}; | ||
// Finally, export `Bookshelf` to the world. | ||
module.exports = Bookshelf; | ||
module.exports = 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). | ||
@@ -12,1 +12,11 @@ * Use the same coding style as the rest of the | ||
* All pull requests should be made to the `master` branch. | ||
### 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`. | ||
Once you have done that, you can run the tests: | ||
```sh | ||
$ npm test | ||
``` |
@@ -6,3 +6,3 @@ // Base Collection | ||
var _ = require('lodash'); | ||
var Backbone = require('backbone'); | ||
var inherits = require('inherits'); | ||
@@ -15,8 +15,9 @@ // All components that need to be referenced in this scope. | ||
var array = []; | ||
var push = array.push; | ||
var slice = array.slice; | ||
var splice = array.splice; | ||
var CollectionBase = function(models, options) { | ||
function CollectionBase(models, options) { | ||
if (options) _.extend(this, _.pick(options, collectionProps)); | ||
this._reset(); | ||
this.initialize.apply(this, arguments); | ||
if (!_.isFunction(this.model)) { | ||
@@ -26,143 +27,352 @@ throw new Error('A valid `model` constructor must be defined for all collections.'); | ||
if (models) this.reset(models, _.extend({silent: true}, options)); | ||
}; | ||
} | ||
inherits(CollectionBase, Events); | ||
// List of attributes attached directly from the constructor's options object. | ||
var collectionProps = ['model', 'comparator']; | ||
var collectionProps = ['model', 'Model', 'comparator']; | ||
// A list of properties that are omitted from the `Backbone.Model.prototype`, to create | ||
// a generic collection base. | ||
var collectionOmitted = ['model', 'fetch', 'url', 'sync', 'create']; | ||
// Copied over from Backbone. | ||
var setOptions = {add: true, remove: true, merge: true}; | ||
var addOptions = {add: true, remove: false}; | ||
_.extend(CollectionBase.prototype, _.omit(Backbone.Collection.prototype, collectionOmitted), Events, { | ||
// The `tableName` on the associated Model, used in relation building. | ||
CollectionBase.prototype.tableName = function() { | ||
return _.result(this.model.prototype, 'tableName'); | ||
}; | ||
// The `tableName` on the associated Model, used in relation building. | ||
tableName: function() { | ||
return _.result(this.model.prototype, 'tableName'); | ||
}, | ||
// The `idAttribute` on the associated Model, used in relation building. | ||
CollectionBase.prototype.idAttribute = function() { | ||
return this.model.prototype.idAttribute; | ||
}; | ||
// The `idAttribute` on the associated Model, used in relation building. | ||
idAttribute: function() { | ||
return this.model.prototype.idAttribute; | ||
}, | ||
CollectionBase.prototype.toString = function() { | ||
return '[Object Collection]'; | ||
}; | ||
// 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. | ||
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 at = options.at; | ||
var targetModel = this.model; | ||
var toAdd = [], toRemove = [], modelMap = {}; | ||
var add = options.add, merge = options.merge, remove = options.remove; | ||
var order = add && remove ? [] : false; | ||
// The JSON representation of a Collection is an array of the | ||
// models' attributes. | ||
CollectionBase.prototype.toJSON = function(options) { | ||
return this.map(function(model){ return model.toJSON(options); }); | ||
}; | ||
// Turn bare objects into model references, and prevent invalid models | ||
// from being added. | ||
for (i = 0, l = models.length; i < l; i++) { | ||
attrs = models[i]; | ||
if (attrs instanceof ModelBase) { | ||
id = model = attrs; | ||
} else { | ||
id = attrs[targetModel.prototype.idAttribute]; | ||
// 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) { | ||
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 at = options.at; | ||
var targetModel = this.model; | ||
var toAdd = [], toRemove = [], modelMap = {}; | ||
var add = options.add, merge = options.merge, remove = options.remove; | ||
var order = add && remove ? [] : false; | ||
// Turn bare objects into model references, and prevent invalid models | ||
// from being added. | ||
for (i = 0, l = models.length; i < l; i++) { | ||
attrs = models[i]; | ||
if (attrs instanceof ModelBase) { | ||
id = model = attrs; | ||
} else { | ||
id = attrs[targetModel.prototype.idAttribute]; | ||
} | ||
// If a duplicate is found, prevent it from being added and | ||
// optionally merge it into the existing model. | ||
if (existing = this.get(id)) { | ||
if (remove) { | ||
modelMap[existing.cid] = true; | ||
} | ||
if (merge) { | ||
attrs = attrs === model ? model.attributes : attrs; | ||
if (options.parse) attrs = existing.parse(attrs, options); | ||
existing.set(attrs, options); | ||
} | ||
// If a duplicate is found, prevent it from being added and | ||
// optionally merge it into the existing model. | ||
if (existing = this.get(id)) { | ||
if (remove) { | ||
modelMap[existing.cid] = true; | ||
} | ||
if (merge) { | ||
attrs = attrs === model ? model.attributes : attrs; | ||
if (options.parse) attrs = existing.parse(attrs, options); | ||
existing.set(attrs, options); | ||
} | ||
// This is a new model, push it to the `toAdd` list. | ||
} else if (add) { | ||
if (!(model = this._prepareModel(attrs, options))) continue; | ||
toAdd.push(model); | ||
// This is a new model, push it to the `toAdd` list. | ||
} else if (add) { | ||
if (!(model = this._prepareModel(attrs, options))) continue; | ||
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; | ||
if (model.id != null) this._byId[model.id] = model; | ||
} | ||
if (order) order.push(existing || 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; | ||
if (model.id != null) this._byId[model.id] = model; | ||
} | ||
if (order) order.push(existing || model); | ||
} | ||
// Remove nonexistent models if appropriate. | ||
if (remove) { | ||
for (i = 0, l = this.length; i < l; ++i) { | ||
if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model); | ||
} | ||
if (toRemove.length) this.remove(toRemove, options); | ||
// Remove nonexistent models if appropriate. | ||
if (remove) { | ||
for (i = 0, l = this.length; i < l; ++i) { | ||
if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model); | ||
} | ||
if (toRemove.length) this.remove(toRemove, options); | ||
} | ||
// See if sorting is needed, update `length` and splice in new models. | ||
if (toAdd.length || (order && order.length)) { | ||
this.length += toAdd.length; | ||
if (at != null) { | ||
splice.apply(this.models, [at, 0].concat(toAdd)); | ||
// See if sorting is needed, update `length` and splice in new models. | ||
if (toAdd.length || (order && order.length)) { | ||
this.length += toAdd.length; | ||
if (at != null) { | ||
splice.apply(this.models, [at, 0].concat(toAdd)); | ||
} else { | ||
if (order) { | ||
this.models.length = 0; | ||
} else { | ||
if (order) { | ||
this.models.length = 0; | ||
} else { | ||
order = toAdd; | ||
} | ||
for (i = 0, l = order.length; i < l; ++i) { | ||
this.models.push(order[i]); | ||
} | ||
order = toAdd; | ||
} | ||
for (i = 0, l = order.length; i < l; ++i) { | ||
this.models.push(order[i]); | ||
} | ||
} | ||
} | ||
if (options.silent) return this; | ||
if (options.silent) return this; | ||
// Trigger `add` events. | ||
for (i = 0, l = toAdd.length; i < l; i++) { | ||
(model = toAdd[i]).trigger('add', model, this, options); | ||
// Trigger `add` events. | ||
for (i = 0, l = toAdd.length; i < l; i++) { | ||
(model = toAdd[i]).trigger('add', model, this, options); | ||
} | ||
return this; | ||
}; | ||
// 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; | ||
return new this.model(attrs, options); | ||
}; | ||
// 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() { | ||
return Promise.all(this.invoke.apply(this, arguments)); | ||
}; | ||
// Run "reduce" over the models in the collection. | ||
CollectionBase.prototype.reduceThen = function(iterator, initialValue, context) { | ||
return Promise.bind(context).thenReturn(this.models).reduce(iterator, initialValue).bind(); | ||
}; | ||
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)); | ||
}; | ||
// Remove a model, or a list of models from the set. | ||
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]); | ||
if (!model) continue; | ||
delete this._byId[model.id]; | ||
delete this._byId[model.cid]; | ||
index = this.indexOf(model); | ||
this.models.splice(index, 1); | ||
this.length--; | ||
if (!options.silent) { | ||
options.index = index; | ||
model.trigger('remove', model, this, options); | ||
} | ||
return this; | ||
}, | ||
this._removeReference(model); | ||
} | ||
return singular ? models[0] : models; | ||
}; | ||
// Prepare a model or hash of attributes to be added to this collection. | ||
_prepareModel: function(attrs, options) { | ||
if (attrs instanceof ModelBase) return attrs; | ||
return new this.model(attrs, options); | ||
}, | ||
// 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) { | ||
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)); | ||
if (!options.silent) this.trigger('reset', this, options); | ||
return models; | ||
}; | ||
// Run "Promise.map" over the models | ||
mapThen: function(iterator, context) { | ||
return Promise.bind(context).thenReturn(this.models).map(iterator); | ||
}, | ||
// Add a model to the end of the collection. | ||
CollectionBase.prototype.push = function(model, options) { | ||
return this.add(model, _.extend({at: this.length}, options)); | ||
}; | ||
// Convenience method for invoke, returning a `Promise.all` promise. | ||
invokeThen: function() { | ||
return Promise.all(this.invoke.apply(this, arguments)); | ||
}, | ||
// Remove a model from the end of the collection. | ||
CollectionBase.prototype.pop = function(options) { | ||
var model = this.at(this.length - 1); | ||
this.remove(model, options); | ||
return model; | ||
}; | ||
// Run "reduce" over the models in the collection. | ||
reduceThen: function(iterator, initialValue, context) { | ||
return Promise.bind(context).thenReturn(this.models).reduce(iterator, initialValue).bind(); | ||
}, | ||
// Add a model to the beginning of the collection. | ||
CollectionBase.prototype.unshift = function(model, options) { | ||
return this.add(model, _.extend({at: 0}, options)); | ||
}; | ||
fetch: function() { | ||
return Promise.rejected('The fetch method has not been implemented'); | ||
// Remove a model from the beginning of the collection. | ||
CollectionBase.prototype.shift = function(options) { | ||
var model = this.at(0); | ||
this.remove(model, options); | ||
return model; | ||
}; | ||
// 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) { | ||
if (obj == null) return void 0; | ||
return this._byId[obj.id] || this._byId[obj.cid] || this._byId[obj]; | ||
}; | ||
// Get the model at the given index. | ||
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) { | ||
if (_.isEmpty(attrs)) return first ? void 0 : []; | ||
return this[first ? 'find' : 'filter'](function(model) { | ||
for (var key in attrs) { | ||
if (attrs[key] !== model.get(key)) return false; | ||
} | ||
return true; | ||
}); | ||
}; | ||
// Return the first model with matching attributes. Useful for simple cases | ||
// of `find`. | ||
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) { | ||
if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); | ||
options || (options = {}); | ||
// Run sort based on type of `comparator`. | ||
if (_.isString(this.comparator) || this.comparator.length === 1) { | ||
this.models = this.sortBy(this.comparator, this); | ||
} else { | ||
this.models.sort(_.bind(this.comparator, this)); | ||
} | ||
if (!options.silent) this.trigger('sort', this, options); | ||
return this; | ||
}; | ||
// Pluck an attribute from each model in the collection. | ||
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) { | ||
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); | ||
}; | ||
// Private method to 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); | ||
}; | ||
// 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. | ||
// 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']; | ||
// Mix in each Underscore method as a proxy to `Collection#models`. | ||
_.each(methods, function(method) { | ||
CollectionBase.prototype[method] = function() { | ||
var args = slice.call(arguments); | ||
args.unshift(this.models); | ||
return _[method].apply(_, args); | ||
}; | ||
}); | ||
// Underscore 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) { | ||
return model.get(value); | ||
}; | ||
return _[method](this.models, iterator, context); | ||
}; | ||
}); | ||
// List of attributes attached directly from the `options` passed to the constructor. | ||
var modelProps = ['tableName', 'hasTimestamps']; | ||
CollectionBase.extend = require('simple-extend'); | ||
CollectionBase.extend = require('../extend'); | ||
module.exports = CollectionBase; |
@@ -10,3 +10,2 @@ // Eager Base | ||
var _ = require('lodash'); | ||
var Backbone = require('backbone'); | ||
var Promise = require('./promise'); | ||
@@ -76,3 +75,3 @@ | ||
// returning the original response when these syncs & pairings are complete. | ||
return Promise.all(pendingDeferred).yield(this.parentResponse); | ||
return Promise.all(pendingDeferred).return(this.parentResponse); | ||
}), | ||
@@ -79,0 +78,0 @@ |
// Events | ||
// --------------- | ||
var Promise = require('./promise'); | ||
var Backbone = require('backbone'); | ||
var triggerThen = require('trigger-then'); | ||
var _ = require('lodash'); | ||
var Promise = require('./promise'); | ||
var inherits = require('inherits'); | ||
var EventEmitter = require('events').EventEmitter; | ||
var _ = require('lodash'); | ||
Backbone.Events.once = function(name, callback, context) { | ||
//if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this; | ||
var self = this; | ||
var once = _.once(function() { | ||
self.off(name, once); | ||
return callback.apply(this, arguments); | ||
}); | ||
once._callback = callback; | ||
return this.on(name, once, context); | ||
function Events() { | ||
EventEmitter.apply(this, arguments); | ||
} | ||
inherits(Events, EventEmitter); | ||
// Regular expression used to split event strings. | ||
var eventSplitter = /\s+/; | ||
Events.prototype.on = function(name, handler) { | ||
// Handle space separated event names. | ||
if (eventSplitter.test(name)) { | ||
var names = name.split(eventSplitter); | ||
for (var i = 0, l = names.length; i < l; i++) { | ||
this.on(names[i], handler); | ||
} | ||
return this; | ||
} | ||
return EventEmitter.prototype.on.apply(this, arguments); | ||
}; | ||
// Mixin the `triggerThen` function into all relevant Backbone objects, | ||
// so we can have event driven async validations, functions, etc. | ||
triggerThen(Backbone, Promise); | ||
// Add "off", "trigger", and "" method, for parity with Backbone.Events | ||
Events.prototype.off = function(event, listener) { | ||
if (arguments.length === 0) { | ||
return this.removeAllListeners(); | ||
} | ||
if (arguments.length === 1) { | ||
return this.removeAllListeners(event); | ||
} | ||
return this.removeListener(event, listener); | ||
}; | ||
Events.prototype.trigger = function(name) { | ||
// Handle space separated event names. | ||
if (eventSplitter.test(name)) { | ||
var len = arguments.length; | ||
var rest = new Array(len - 1); | ||
for (i = 1; i < len; i++) rest[i - 1] = arguments[i]; | ||
var names = name.split(eventSplitter); | ||
for (var i = 0, l = names.length; i < l; i++) { | ||
EventEmitter.prototype.emit.apply(this, [names[i]].concat(rest)); | ||
} | ||
return this; | ||
} | ||
EventEmitter.prototype.emit.apply(this, arguments); | ||
return this; | ||
}; | ||
module.exports = Backbone.Events; | ||
Events.prototype.triggerThen = function(name) { | ||
var i, l, rest, listeners = []; | ||
// Handle space separated event names. | ||
if (eventSplitter.test(name)) { | ||
var names = name.split(eventSplitter); | ||
for (i = 0, l = names.length; i < l; i++) { | ||
listeners = listeners.concat(this.listeners(names[i])); | ||
} | ||
} else { | ||
listeners = this.listeners(name); | ||
} | ||
var len = arguments.length; | ||
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]; | ||
} | ||
var events = this | ||
return Promise.try(function() { | ||
var pending = []; | ||
for (i = 0, l = listeners.length; i < l; i++) { | ||
pending[i] = listeners[i].apply(events, rest); | ||
} | ||
return Promise.all(pending); | ||
}) | ||
}; | ||
Events.prototype.emitThen = Events.prototype.triggerThen; | ||
Events.prototype.once = function(name, callback, context) { | ||
var self = this; | ||
var once = _.once(function() { | ||
self.off(name, once); | ||
return callback.apply(this, arguments); | ||
}); | ||
once._callback = callback; | ||
return this.on(name, once, context); | ||
}; | ||
module.exports = Events; |
// Base Model | ||
// --------------- | ||
var _ = require('lodash'); | ||
var Backbone = require('backbone'); | ||
var inherits = require('inherits'); | ||
var Events = require('./events'); | ||
var Promise = require('./promise'); | ||
var Errors = require('../errors'); | ||
var slice = Array.prototype.slice | ||
// A list of properties that are omitted from the `Backbone.Model.prototype`, to create | ||
// a generic model base. | ||
var modelOmitted = [ | ||
'changedAttributes', 'isValid', 'validationError', | ||
'save', 'sync', 'fetch', 'destroy', 'url', | ||
'urlRoot', '_validate' | ||
]; | ||
// List of attributes attached directly from the `options` passed to the constructor. | ||
@@ -21,7 +15,6 @@ var modelProps = ['tableName', 'hasTimestamps']; | ||
// The "ModelBase" is similar to the 'Active Model' in Rails, | ||
// it defines a standard interface from which other objects may | ||
// inherit. | ||
var ModelBase = function(attributes, options) { | ||
// 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); | ||
@@ -37,154 +30,198 @@ this._reset(); | ||
this.initialize.apply(this, arguments); | ||
} | ||
inherits(ModelBase, Events); | ||
ModelBase.prototype.initialize = function() {}; | ||
// The default value for the "id" attribute. | ||
ModelBase.prototype.idAttribute = 'id'; | ||
// Get the value of an attribute. | ||
ModelBase.prototype.get = function(attr) { | ||
return this.attributes[attr]; | ||
}; | ||
_.extend(ModelBase.prototype, _.omit(Backbone.Model.prototype, modelOmitted), Events, { | ||
// Set a property. | ||
ModelBase.prototype.set = function(key, val, options) { | ||
if (key == null) return this; | ||
var attrs; | ||
// Similar to the standard `Backbone` set method, but without individual | ||
// change events, and adding different meaning to `changed` and `previousAttributes` | ||
// defined as the last "sync"'ed state of the model. | ||
set: function(key, val, options) { | ||
if (key == null) return this; | ||
var attrs; | ||
// Handle both `"key", value` and `{key: value}` -style arguments. | ||
if (typeof key === 'object') { | ||
attrs = key; | ||
options = val; | ||
} else { | ||
(attrs = {})[key] = val; | ||
} | ||
options = _.clone(options) || {}; | ||
// Handle both `"key", value` and `{key: value}` -style arguments. | ||
if (typeof key === 'object') { | ||
attrs = key; | ||
options = val; | ||
// Extract attributes and options. | ||
var hasChanged = false; | ||
var unset = options.unset; | ||
var current = this.attributes; | ||
var prev = this._previousAttributes; | ||
// Check for changes of `id`. | ||
if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; | ||
// For each `set` attribute, update or delete the current value. | ||
for (var attr in attrs) { | ||
val = attrs[attr]; | ||
if (!_.isEqual(prev[attr], val)) { | ||
this.changed[attr] = val; | ||
if (!_.isEqual(current[attr], val)) hasChanged = true; | ||
} else { | ||
(attrs = {})[key] = val; | ||
delete this.changed[attr]; | ||
} | ||
options || (options = {}); | ||
unset ? delete current[attr] : current[attr] = val; | ||
} | ||
return this; | ||
}; | ||
// Extract attributes and options. | ||
var hasChanged = false; | ||
var unset = options.unset; | ||
var current = this.attributes; | ||
var prev = this._previousAttributes; | ||
// A model is new if it has never been persisted, which we assume if it lacks an id. | ||
ModelBase.prototype.isNew = function() { | ||
return this.id == null; | ||
}; | ||
// Check for changes of `id`. | ||
if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; | ||
// For each `set` attribute, update or delete the current value. | ||
for (var attr in attrs) { | ||
val = attrs[attr]; | ||
if (!_.isEqual(prev[attr], val)) { | ||
this.changed[attr] = val; | ||
if (!_.isEqual(current[attr], val)) hasChanged = true; | ||
} else { | ||
delete this.changed[attr]; | ||
} | ||
unset ? delete current[attr] : current[attr] = val; | ||
// 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`. | ||
ModelBase.prototype.toJSON = function(options) { | ||
var attrs = _.clone(this.attributes); | ||
if (options && options.shallow) return attrs; | ||
var relations = this.relations; | ||
for (var key in relations) { | ||
var relation = relations[key]; | ||
attrs[key] = relation.toJSON ? relation.toJSON(options) : relation; | ||
} | ||
if (this.pivot) { | ||
var pivot = this.pivot.attributes; | ||
for (key in pivot) { | ||
attrs['_pivot_' + key] = pivot[key]; | ||
} | ||
} | ||
return attrs; | ||
}; | ||
if (hasChanged && !options.silent) this.trigger('change', this, options); | ||
return this; | ||
}, | ||
// Returns the string representation of the object. | ||
ModelBase.prototype.toString = function() { | ||
return '[Object Model]'; | ||
}; | ||
// 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`. | ||
toJSON: function(options) { | ||
var attrs = _.extend({}, this.attributes); | ||
if (options && options.shallow) return attrs; | ||
var relations = this.relations; | ||
for (var key in relations) { | ||
var relation = relations[key]; | ||
attrs[key] = relation.toJSON ? relation.toJSON(options) : relation; | ||
} | ||
if (options && options.omitPivot) return attrs; | ||
if (this.pivot) { | ||
var pivot = this.pivot.attributes; | ||
for (key in pivot) { | ||
attrs['_pivot_' + key] = pivot[key]; | ||
} | ||
} | ||
return attrs; | ||
}, | ||
// Get the HTML-escaped value of an attribute. | ||
ModelBase.prototype.escape = function(key) { | ||
return _.escape(this.get(key)); | ||
}; | ||
// **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. | ||
parse: function(resp, options) { | ||
return resp; | ||
}, | ||
// Returns `true` if the attribute contains a value that is not null | ||
// or undefined. | ||
ModelBase.prototype.has = function(attr) { | ||
return this.get(attr) != null; | ||
}; | ||
// **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. | ||
format: function(attrs, options) { | ||
return attrs; | ||
}, | ||
// **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) { | ||
return resp; | ||
}; | ||
// Returns the related item, or creates a new | ||
// related item by creating a new model or collection. | ||
related: function(name) { | ||
return this.relations[name] || (this[name] ? this.relations[name] = this[name]() : void 0); | ||
}, | ||
// 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})); | ||
}; | ||
// Create a new model with identical attributes to this one, | ||
// including any relations on the current model. | ||
clone: function() { | ||
var model = new this.constructor(this.attributes); | ||
var relations = this.relations; | ||
for (var key in relations) { | ||
model.relations[key] = relations[key].clone(); | ||
} | ||
model._previousAttributes = _.clone(this._previousAttributes); | ||
model.changed = _.clone(this.changed); | ||
return model; | ||
}, | ||
// Clear all attributes on the model, firing `"change"`. | ||
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})); | ||
}; | ||
// Sets the timestamps before saving the model. | ||
timestamp: function(options) { | ||
var d = new Date(); | ||
var keys = (_.isArray(this.hasTimestamps) ? this.hasTimestamps : ['created_at', 'updated_at']); | ||
var vals = {}; | ||
if (keys[0]) { | ||
if (this.isNew(options)) { | ||
if (!options || options.method !== 'update') { | ||
vals[keys[0]] = d; | ||
} | ||
} | ||
else if (options && options.method === 'insert') { | ||
vals[keys[0]] = d; | ||
} | ||
} | ||
if (keys[1]) vals[keys[1]] = d; | ||
return vals; | ||
}, | ||
// **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) { | ||
return attrs; | ||
}; | ||
// Called after a `sync` action (save, fetch, delete) - | ||
// resets the `_previousAttributes` and `changed` hash for the model. | ||
_reset: function() { | ||
this._previousAttributes = _.extend(Object.create(null), this.attributes); | ||
this.changed = Object.create(null); | ||
return this; | ||
}, | ||
// Returns the related item, or creates a new | ||
// related item by creating a new model or collection. | ||
ModelBase.prototype.related = function(name) { | ||
return this.relations[name] || (this[name] ? this.relations[name] = this[name]() : void 0); | ||
}; | ||
fetch: function() {}, | ||
// Create a new model with identical attributes to this one, | ||
// including any relations on the current model. | ||
ModelBase.prototype.clone = function(options) { | ||
var model = new this.constructor(this.attributes); | ||
var relations = this.relations; | ||
for (var key in relations) { | ||
model.relations[key] = relations[key].clone(); | ||
} | ||
model._previousAttributes = _.clone(this._previousAttributes); | ||
model.changed = setProps(Object.create(null), this.changed); | ||
return model; | ||
}; | ||
save: function() {}, | ||
// 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; | ||
}; | ||
// 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) : {}; | ||
var sync = this.sync(options); | ||
options.query = sync.query; | ||
return Promise.bind(this).then(function() { | ||
return this.triggerThen('destroying', this, options); | ||
}).then(function() { | ||
return sync.del(); | ||
}).then(function(resp) { | ||
this.clear(); | ||
return this.triggerThen('destroyed', this, resp, options); | ||
}).then(this._reset); | ||
}) | ||
// 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) { | ||
if (attr == null) return !_.isEmpty(this.changed); | ||
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) { | ||
if (attr == null || !this._previousAttributes) return null; | ||
return this._previousAttributes[attr]; | ||
}; | ||
// Get all of the attributes of the model at the time of the previous | ||
// `"change"` event. | ||
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() { | ||
this._previousAttributes = _.clone(this.attributes); | ||
this.changed = Object.create(null); | ||
return this; | ||
}; | ||
// 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]; | ||
} | ||
} | ||
// "_" methods that we want to implement on the Model. | ||
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() { | ||
var args = slice.call(arguments); | ||
args.unshift(this.attributes); | ||
return _[method].apply(_, args); | ||
}; | ||
}); | ||
ModelBase.extend = require('simple-extend'); | ||
ModelBase.extend = require('../extend'); | ||
module.exports = ModelBase; |
var Promise = require('bluebird/js/main/promise')(); | ||
var helpers = require('../helpers') | ||
Promise.prototype.yield = Promise.prototype.return; | ||
Promise.prototype.ensure = Promise.prototype.lastly; | ||
Promise.prototype.otherwise = Promise.prototype.caught; | ||
Promise.prototype.exec = Promise.prototype.nodeify; | ||
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); | ||
}; | ||
Promise.resolve = Promise.fulfilled; | ||
Promise.reject = Promise.rejected; | ||
module.exports = Promise; |
@@ -5,3 +5,2 @@ // Base Relation | ||
var _ = require('lodash'); | ||
var Backbone = require('backbone'); | ||
var CollectionBase = require('./collection'); | ||
@@ -42,4 +41,4 @@ | ||
RelationBase.extend = Backbone.Model.extend; | ||
RelationBase.extend = require('../extend'); | ||
module.exports = RelationBase; |
// Collection | ||
// --------------- | ||
var _ = require('lodash'); | ||
var inherits = require('inherits'); | ||
@@ -13,12 +12,6 @@ var Sync = require('./sync'); | ||
var Promise = require('./base/promise'); | ||
var createError = require('create-error'); | ||
function BookshelfCollection() { | ||
CollectionBase.apply(this, arguments); | ||
} | ||
inherits(BookshelfCollection, CollectionBase); | ||
var BookshelfCollection = CollectionBase.extend({ | ||
BookshelfCollection.EmptyError = Errors.EmptyError; | ||
_.extend(BookshelfCollection.prototype, { | ||
// Used to define passthrough relationships - `hasOne`, `hasMany`, | ||
@@ -39,3 +32,3 @@ // `belongsTo` or `belongsToMany`, "through" a `Interim` model or collection. | ||
if (!response || response.length === 0) { | ||
if (options.require) throw new Errors.EmptyError('EmptyResponse'); | ||
if (options.require) throw new this.constructor.EmptyError('EmptyResponse'); | ||
return Promise.reject(null); | ||
@@ -145,4 +138,12 @@ } | ||
}, { | ||
extended: function(child) { | ||
child.EmptyError = createError(this.EmptyError) | ||
} | ||
}); | ||
BookshelfCollection.EmptyError = Errors.EmptyError | ||
module.exports = BookshelfCollection; |
@@ -10,4 +10,10 @@ var createError = require('create-error'); | ||
// collection.fetch | ||
EmptyError: createError('EmptyError') | ||
EmptyError: createError('EmptyError'), | ||
}; | ||
// Thrown when an update affects no rows and {require: true} is passed in model.save. | ||
NoRowsUpdatedError: createError('NoRowsUpdatedError'), | ||
// Thrown when a delete affects no rows and {require: true} is passed in model.destroy. | ||
NoRowsDeletedError: createError('NoRowsDeletedError') | ||
}; |
// Helpers | ||
// --------------- | ||
var _ = require('lodash'); | ||
var _ = require('lodash'); | ||
var chalk = require('chalk') | ||
module.exports = { | ||
var helpers = { | ||
@@ -49,4 +50,19 @@ // Sets the constraints necessary during a `model.save` call. | ||
return obj; | ||
}, | ||
error: function(msg) { | ||
console.log(chalk.red(msg)) | ||
}, | ||
warn: function(msg) { | ||
console.log(chalk.yellow(msg)) | ||
}, | ||
deprecate: function(a, b) { | ||
helpers.warn(a + ' has been deprecated, please use ' + b + ' instead') | ||
} | ||
}; | ||
module.exports = helpers |
// Model | ||
// --------------- | ||
var _ = require('lodash'); | ||
var inherits = require('inherits'); | ||
var createError = require('create-error') | ||
@@ -14,11 +14,4 @@ var Sync = require('./sync'); | ||
function BookshelfModel() { | ||
ModelBase.apply(this, arguments); | ||
} | ||
inherits(BookshelfModel, ModelBase); | ||
var BookshelfModel = ModelBase.extend({ | ||
BookshelfModel.NotFoundError = Errors.NotFoundError; | ||
_.extend(BookshelfModel.prototype, { | ||
// The `hasOne` relation specifies that this table has exactly one of another type of object, | ||
@@ -101,3 +94,3 @@ // specified by a foreign key in the other table. The foreign key is assumed to be the singular of this | ||
if (!response || response.length === 0) { | ||
if (options.require) throw new Errors.NotFoundError('EmptyResponse'); | ||
if (options.require) throw new this.constructor.NotFoundError('EmptyResponse'); | ||
return Promise.reject(null); | ||
@@ -225,3 +218,5 @@ } | ||
} else if (method === 'update' && resp === 0) { | ||
throw new Error('No rows were affected in the update, did you mean to pass the {method: "insert"} option?'); | ||
if (options.require !== false) { | ||
throw new this.constructor.NoRowsUpdatedError('No Rows Updated'); | ||
} | ||
} | ||
@@ -237,3 +232,2 @@ | ||
}); | ||
}) | ||
@@ -243,2 +237,23 @@ .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) : {}; | ||
var sync = this.sync(options); | ||
options.query = sync.query; | ||
return Promise.bind(this).then(function() { | ||
return this.triggerThen('destroying', this, options); | ||
}).then(function() { | ||
return sync.del(); | ||
}).then(function(resp) { | ||
if (options.require && resp === 0) { | ||
throw new this.constructor.NoRowsDeletedError('No Rows Deleted'); | ||
} | ||
this.clear(); | ||
return this.triggerThen('destroyed', this, resp, options); | ||
}).then(this._reset); | ||
}), | ||
// Reset the query builder, called internally | ||
@@ -295,4 +310,16 @@ // each time a query is run. | ||
}, { | ||
extended: function(child) { | ||
child.NotFoundError = createError(this.NotFoundError) | ||
child.NoRowsUpdatedError = createError(this.NoRowsUpdatedError) | ||
child.NoRowsDeletedError = createError(this.NoRowsDeletedError) | ||
} | ||
}); | ||
BookshelfModel.NotFoundError = Errors.NotFoundError, | ||
BookshelfModel.NoRowsUpdatedError = Errors.NoRowsUpdatedError, | ||
BookshelfModel.NoRowsDeletedError = Errors.NoRowsDeletedError | ||
module.exports = BookshelfModel; |
@@ -14,9 +14,4 @@ // Relation | ||
function BookshelfRelation() { | ||
RelationBase.apply(this, arguments); | ||
} | ||
inherits(BookshelfRelation, RelationBase); | ||
var BookshelfRelation = RelationBase.extend({ | ||
_.extend(BookshelfRelation.prototype, { | ||
// Assembles the new model or collection we're creating an instance of, | ||
@@ -137,3 +132,3 @@ // gathering any relevant primitives from the parent object, | ||
if (this.isJoined()) this.joinColumns(knex); | ||
// If this is a single relation and we're not eager loading, | ||
@@ -383,2 +378,4 @@ // limit the query to a single item. | ||
return this.triggerThen('attached', this, resp, options); | ||
}).then(function() { | ||
return this; | ||
}); | ||
@@ -429,21 +426,17 @@ }, | ||
} | ||
return Promise.all(pending).yield(this); | ||
return Promise.all(pending).return(this); | ||
}), | ||
// Handles setting the appropriate constraints and shelling out | ||
// to either the `insert` or `delete` call for the current model, | ||
// returning a promise. | ||
// Handles preparing the appropriate constraints and then delegates | ||
// the database interaction to _processPlainPivot for non-.through() | ||
// pivot definitions, or _processModelPivot for .through() models. | ||
// Returns a promise. | ||
_processPivot: Promise.method(function(method, item, options) { | ||
var data = {}; | ||
var relatedData = this.relatedData; | ||
var relatedData = this.relatedData | ||
, args = Array.prototype.slice.call(arguments) | ||
, fks = {} | ||
, data = {}; | ||
// Grab the `knex` query builder for the current model, and | ||
// check if we have any additional constraints for the query. | ||
var builder = this._builder(relatedData.joinTable()); | ||
if (options && options.query) { | ||
Helpers.query.call(null, {_knex: builder}, [options.query]); | ||
} | ||
fks[relatedData.key('foreignKey')] = relatedData.parentFk; | ||
data[relatedData.key('foreignKey')] = relatedData.parentFk; | ||
// If the item is an object, it's either a model | ||
@@ -454,3 +447,3 @@ // that we're looking to attach to this model, or | ||
if (item instanceof ModelBase) { | ||
data[relatedData.key('otherKey')] = item.id; | ||
fks[relatedData.key('otherKey')] = item.id; | ||
} else if (method !== 'update') { | ||
@@ -460,5 +453,27 @@ _.extend(data, item); | ||
} else if (item) { | ||
data[relatedData.key('otherKey')] = item; | ||
fks[relatedData.key('otherKey')] = item; | ||
} | ||
args.push(_.extend(data, fks), fks); | ||
if (this.relatedData.throughTarget) { | ||
return this._processModelPivot.apply(this, args); | ||
} | ||
return this._processPlainPivot.apply(this, args); | ||
}), | ||
// Applies constraints to the knex builder and handles shelling out | ||
// to either the `insert` or `delete` call for the current model, | ||
// returning a promise. | ||
_processPlainPivot: Promise.method(function(method, item, options, data, fks) { | ||
var relatedData = this.relatedData; | ||
// Grab the `knex` query builder for the current model, and | ||
// check if we have any additional constraints for the query. | ||
var builder = this._builder(relatedData.joinTable()); | ||
if (options && options.query) { | ||
Helpers.query.call(null, {_knex: builder}, [options.query]); | ||
} | ||
if (options) { | ||
@@ -468,2 +483,3 @@ if (options.transacting) builder.transacting(options.transacting); | ||
} | ||
var collection = this; | ||
@@ -491,2 +507,28 @@ if (method === 'delete') { | ||
}); | ||
}), | ||
// Loads or prepares a pivot model based on the constraints and deals with | ||
// pivot model changes by calling the appropriate Bookshelf Model API | ||
// methods. Returns a promise. | ||
_processModelPivot: Promise.method(function(method, item, options, data, fks) { | ||
var relatedData = this.relatedData | ||
, JoinModel = relatedData.throughTarget | ||
, instance = new JoinModel; | ||
fks = instance.parse(fks); | ||
data = instance.parse(data); | ||
if (method === 'insert') { | ||
return instance.set(data).save(null, options); | ||
} | ||
return instance.set(fks).fetch({ | ||
require: true | ||
}).then(function (instance) { | ||
if (method === 'delete') { | ||
return instance.destroy(options); | ||
} | ||
return instance.save(item, options); | ||
}); | ||
}) | ||
@@ -493,0 +535,0 @@ |
@@ -47,15 +47,22 @@ // Sync | ||
select: Promise.method(function() { | ||
var columns, sync = this, | ||
options = this.options, relatedData = this.syncing.relatedData; | ||
var knex = this.query; | ||
var knex = this.query | ||
, options = this.options | ||
, relatedData = this.syncing.relatedData | ||
, columnsInQuery = _.some(knex._statements, {grouping:'columns'}) | ||
, columns; | ||
// Inject all appropriate select costraints dealing with the relation | ||
// into the `knex` query builder for the current instance. | ||
if (relatedData) { | ||
relatedData.selectConstraints(knex, options); | ||
} else { | ||
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] : [_.result(this.syncing, 'tableName') + '.*']; | ||
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') + '.*']; | ||
} | ||
} | ||
@@ -67,4 +74,22 @@ | ||
// Trigger a `fetching` event on the model, and then select the appropriate columns. | ||
return Promise.bind(this).then(function() { | ||
return Promise.bind(this).then(function () { | ||
var fks = {} | ||
, through; | ||
// Inject all appropriate select costraints dealing with the relation | ||
// into the `knex` query builder for the current instance. | ||
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); | ||
} | ||
} | ||
}).then(function () { | ||
return this.syncing.triggerThen('fetching', this.syncing, columns, options); | ||
@@ -104,2 +129,2 @@ }).then(function() { | ||
module.exports = Sync; | ||
module.exports = Sync; |
{ | ||
"name": "bookshelf", | ||
"version": "0.7.9", | ||
"version": "0.8.0", | ||
"description": "A lightweight ORM for PostgreSQL, MySQL, and SQLite3", | ||
"main": "bookshelf.js", | ||
"scripts": { | ||
"build": "./scripts/build.sh", | ||
"jshint": "jshint bookshelf.js lib/*", | ||
"test": "mocha -b -t 5000 --check-leaks -R spec test/index.js", | ||
"release:patch": "npm run jshint && npm test && gulp build && gulp bump --type patch && gulp release", | ||
"release:minor": "npm run jshint && npm test && gulp build && gulp bump --type minor && gulp release" | ||
"test": "mocha -b -t 5000 --check-leaks -R spec test/index.js" | ||
}, | ||
@@ -26,22 +25,14 @@ "homepage": "http://bookshelfjs.org", | ||
"dependencies": { | ||
"backbone": "1.1.0", | ||
"bluebird": "^2.0.0", | ||
"bluebird": "^2.9.4", | ||
"chalk": "^1.0.0", | ||
"create-error": "~0.3.1", | ||
"inflection": "^1.5.1", | ||
"inherits": "~2.0.1", | ||
"lodash": "^2.0.0", | ||
"semver": "^4.1.0", | ||
"simple-extend": "0.1.0", | ||
"trigger-then": "0.3.x" | ||
"lodash": "^3.7.0", | ||
"semver": "^4.3.3" | ||
}, | ||
"devDependencies": { | ||
"browserify": "^6.2.0", | ||
"chai": "~1.9.1", | ||
"chai-as-promised": "~4.1.0", | ||
"gulp": "^3.6.2", | ||
"gulp-bump": "^0.1.8", | ||
"gulp-git": "^0.5.3", | ||
"gulp-shell": "^0.2.5", | ||
"jshint": "~2.5.1", | ||
"knex": "0.7.x", | ||
"jshint": "~2.7.0", | ||
"knex": "^0.8.0", | ||
"minimist": "^1.1.0", | ||
@@ -51,8 +42,6 @@ "mocha": "^2.0.1", | ||
"node-uuid": "~1.4.1", | ||
"pg": "^3.6.2", | ||
"pg": "^4.3.0", | ||
"sinon": "^1.11.1", | ||
"sinon-chai": "^2.6.0", | ||
"sqlite3": "^3.0.2", | ||
"through": "^2.3.4", | ||
"underscore.string": "~2.3.1" | ||
"sqlite3": "^3.0.5" | ||
}, | ||
@@ -59,0 +48,0 @@ "author": { |
@@ -33,3 +33,3 @@ // Registry Plugin - | ||
if (_.isPlainObject(CollectionCtor)) { | ||
CollectionCtor = this.Model.extend(CollectionCtor, staticProps); | ||
CollectionCtor = this.Collection.extend(CollectionCtor, staticProps); | ||
} | ||
@@ -36,0 +36,0 @@ this._collections[name] = CollectionCtor; |
@@ -104,4 +104,6 @@ // Virtuals Plugin | ||
var virtual = this.virtuals && this.virtuals[key]; | ||
if (virtual && virtual.set) { | ||
virtual.set.call(this, value); | ||
if (virtual) { | ||
if (virtual.set) { | ||
virtual.set.call(this, value); | ||
} | ||
return true; | ||
@@ -108,0 +110,0 @@ } |
@@ -1,6 +0,6 @@ | ||
# [bookshelf.js](http://bookshelfjs.org) [![Build Status](https://travis-ci.org/tgriesser/bookshelf.png?branch=master)](https://travis-ci.org/tgriesser/bookshelf) | ||
# [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. | ||
It is built atop the <a href="http://knexjs.org">Knex Query Builder</a>, | ||
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. | ||
@@ -10,5 +10,5 @@ | ||
For Docs, License, Tests, FAQ, and other information, see: http://bookshelfjs.org. | ||
For documentation, FAQs, and other information, see: http://bookshelfjs.org. | ||
To suggest a feature, report a bug, or general discussion: http://github.com/tgriesser/bookshelf/issues/ | ||
To suggest a feature, report a bug, or for general discussion: http://github.com/tgriesser/bookshelf/issues/ | ||
@@ -24,3 +24,3 @@ ## Examples | ||
var User = bookshelf.Model.extend({ | ||
tableName: 'users' | ||
tableName: 'users', | ||
messages: function() { | ||
@@ -51,2 +51,6 @@ return this.hasMany(Posts); | ||
}); | ||
``` | ||
``` | ||
## Contributing | ||
To contribute to Bookshelf, read the [contribution documentation](CONTRIBUTING.md) for more information. |
@@ -28,3 +28,3 @@ var Promise = require('../lib/base/promise'); | ||
chai.use(require('chai-as-promised')); | ||
// chai.use(require('chai-as-promised')); | ||
chai.use(require('sinon-chai')); | ||
@@ -31,0 +31,0 @@ chai.should(); |
@@ -1,3 +0,7 @@ | ||
var Promise = global.testPromise; | ||
var Promise = global.testPromise; | ||
var assert = require('assert') | ||
var equal = assert.equal | ||
var deepEqual = assert.deepEqual | ||
module.exports = function(bookshelf) { | ||
@@ -7,4 +11,2 @@ | ||
var Backbone = require('backbone'); | ||
var output = require('./output/Collection'); | ||
@@ -36,2 +38,15 @@ var dialect = bookshelf.knex.client.dialect; | ||
describe('extend', function() { | ||
it ('should have own EmptyError', function() { | ||
var Sites = bookshelf.Collection.extend({model: Site}); | ||
var OtherSites = bookshelf.Collection.extend({model: Site}); | ||
var err = new Sites.EmptyError(); | ||
expect(Sites.EmptyError).to.not.be.eql(bookshelf.Collection.EmptyError); | ||
expect(Sites.EmptyError).to.not.be.eql(OtherSites.EmptyError); | ||
expect(Sites.EmptyError).to.not.be.eql(OtherSites.EmptyError); | ||
expect(err).to.be.an.instanceof(bookshelf.Collection.EmptyError); | ||
}); | ||
}); | ||
describe('fetch', function() { | ||
@@ -66,3 +81,3 @@ | ||
.then(function(model) { | ||
expect(model).to.be.null; | ||
equal(model, null); | ||
}); | ||
@@ -74,6 +89,10 @@ | ||
return expect(new Site({id:1}) | ||
return new Site({id:1}) | ||
.authors() | ||
.query({where: {id: 40}}) | ||
.fetchOne({require: true})).to.be.rejected; | ||
.fetchOne({require: true}) | ||
.throw(new Error()) | ||
.catch(function(err) { | ||
assert(err instanceof Author.NotFoundError, 'Error is a Site.NotFoundError') | ||
}); | ||
@@ -88,3 +107,3 @@ }); | ||
var model = new bookshelf.Model(); | ||
expect(model.sync(model)).to.be.an.instanceOf(require('../../lib/sync')); | ||
assert(model.sync(model) instanceof require('../../lib/sync')); | ||
}); | ||
@@ -91,0 +110,0 @@ |
@@ -10,3 +10,4 @@ var _ = require('lodash'); | ||
'Customer', 'Settings', 'hostnames', 'instances', 'uuid_test', | ||
'parsed_users', 'tokens', 'thumbnails' | ||
'parsed_users', 'tokens', 'thumbnails', | ||
'lefts', 'rights', 'lefts_rights' | ||
]; | ||
@@ -148,2 +149,15 @@ | ||
table.string('token'); | ||
}) | ||
// | ||
.createTable('lefts', function(table) { | ||
table.increments(); | ||
}) | ||
.createTable('rights', function(table) { | ||
table.increments(); | ||
}) | ||
.createTable('lefts_rights', function(table) { | ||
table.increments(); | ||
table.string('parsed_name'); | ||
table.integer('left_id').notNullable(); | ||
table.integer('right_id').notNullable(); | ||
}); | ||
@@ -150,0 +164,0 @@ |
@@ -6,3 +6,2 @@ // All Models & Collections Used in the Tests | ||
var _ = require('lodash'); | ||
_.str = require('underscore.string'); | ||
@@ -268,3 +267,3 @@ function _parsed (attributes) { | ||
return _.transform(attrs, function (result, val, key) { | ||
result[_.str.underscored(key)] = val; | ||
result[_.snakeCase(key)] = val; | ||
}); | ||
@@ -274,3 +273,3 @@ }, | ||
return _.transform(attrs, function (result, val, key) { | ||
result[_.str.camelize(key)] = val; | ||
result[_.camelCase(key)] = val; | ||
}); | ||
@@ -293,2 +292,45 @@ } | ||
/** | ||
* Issue #578 - lifecycle events on pivot model for belongsToMany().through() | ||
* | ||
* Here we bootstrap some models involved in a .belongsToMany().through() | ||
* relationship. The models are overridden with actual relationship methods | ||
* e.g. `lefts: function () { return this.belongsToMany(LeftModel).through(JoinModel) }` | ||
* within the tests to ensure the appropriate lifecycle events are being | ||
* triggered. | ||
*/ | ||
var LeftModel = Bookshelf.Model.extend({ | ||
tableName: 'lefts' | ||
}); | ||
var RightModel = Bookshelf.Model.extend({ | ||
tableName: 'rights' | ||
}); | ||
var JoinModel = Bookshelf.Model.extend({ | ||
tableName: 'lefts_rights', | ||
defaults: { parsedName: '' }, | ||
format: function (attrs) { | ||
return _.reduce(attrs, function(memo, val, key) { | ||
memo[_.snakeCase(key)] = val; | ||
return memo; | ||
}, {}); | ||
}, | ||
parse: function (attrs) { | ||
return _.reduce(attrs, function(memo, val, key) { | ||
memo[_.camelCase(key)] = val; | ||
return memo; | ||
}, {}); | ||
}, | ||
lefts: function() { | ||
return this.belongsTo(LeftModel); | ||
}, | ||
rights: function() { | ||
return this.belongsTo(RightModel); | ||
} | ||
}); | ||
return { | ||
@@ -319,3 +361,6 @@ Models: { | ||
Hostname: Hostname, | ||
Uuid: Uuid | ||
Uuid: Uuid, | ||
LeftModel: LeftModel, | ||
RightModel: RightModel, | ||
JoinModel: JoinModel | ||
} | ||
@@ -322,0 +367,0 @@ }; |
var _ = require('lodash'); | ||
var uuid = require('node-uuid'); | ||
_.str = require('underscore.string'); | ||
var Promise = global.testPromise; | ||
var Promise = global.testPromise; | ||
var equal = require('assert').equal; | ||
var assert = require('assert') | ||
var equal = require('assert').equal; | ||
var deepEqual = require('assert').deepEqual; | ||
@@ -14,3 +14,2 @@ | ||
var Backbone = require('backbone'); | ||
var Models = require('./helpers/objects')(bookshelf).Models; | ||
@@ -41,4 +40,11 @@ | ||
var OtherUser = bookshelf.Model.extend({ | ||
idAttribute: 'user_id', | ||
getData: function() { return 'test'; } | ||
}, { | ||
classMethod: function() { return 'test'; } | ||
}); | ||
it('can be extended', function() { | ||
var user = new User(); | ||
var user = new User({name: "hoge"}); | ||
var subUser = new SubUser(); | ||
@@ -64,6 +70,43 @@ expect(user.idAttribute).to.equal('user_id'); | ||
it('doesnt have ommitted Backbone properties', function() { | ||
expect(User.prototype.changedAttributes).to.be.undefined; | ||
expect((new User()).changedAttributes).to.be.undefined; | ||
equal(User.prototype.changedAttributes, undefined); | ||
equal((new User()).changedAttributes, undefined); | ||
}); | ||
context('should have own errors: name of', function(){ | ||
it('NotFoundError', function(){ | ||
var err = new User.NotFoundError(); | ||
var suberr = new SubUser.NotFoundError(); | ||
expect(User.NotFoundError).to.not.be.eql(bookshelf.Model.NotFoundError); | ||
expect(err).to.be.an.instanceof(bookshelf.Model.NotFoundError); | ||
expect(User.NotFoundError).to.not.be.eql(SubUser.NotFoundError); | ||
expect(err).to.not.be.an.instanceof(SubUser.NotFoundError); | ||
expect(suberr).to.be.an.instanceof(User.NotFoundError); | ||
expect(User.NotFoundError).to.not.be.eql(OtherUser.NotFoundError); | ||
expect(err).to.not.be.an.instanceof(OtherUser.NotFoundError); | ||
}); | ||
it('NoRowsUpdatedError', function(){ | ||
var err = new User.NoRowsUpdatedError(); | ||
var suberr = new SubUser.NoRowsUpdatedError(); | ||
expect(User.NoRowsUpdatedError).to.not.be.eql(bookshelf.Model.NoRowsUpdatedError); | ||
expect(err).to.be.an.instanceof(bookshelf.Model.NoRowsUpdatedError); | ||
expect(User.NoRowsUpdatedError).to.not.be.eql(SubUser.NoRowsUpdatedError); | ||
expect(err).to.not.be.an.instanceof(SubUser.NoRowsUpdatedError); | ||
expect(suberr).to.be.an.instanceof(User.NoRowsUpdatedError); | ||
expect(User.NoRowsUpdatedError).to.not.be.eql(OtherUser.NoRowsUpdatedError); | ||
expect(err).to.not.be.an.instanceof(OtherUser.NoRowsUpdatedError); | ||
}); | ||
it('NoRowsDeletedError', function(){ | ||
var err = new User.NoRowsDeletedError(); | ||
var suberr = new SubUser.NoRowsDeletedError(); | ||
expect(User.NoRowsDeletedError).to.not.be.eql(bookshelf.Model.NoRowsDeletedError); | ||
expect(err).to.be.an.instanceof(bookshelf.Model.NoRowsDeletedError); | ||
expect(User.NoRowsDeletedError).to.not.be.eql(SubUser.NoRowsDeletedError); | ||
expect(err).to.not.be.an.instanceof(SubUser.NoRowsDeletedError); | ||
expect(suberr).to.be.an.instanceof(User.NoRowsDeletedError); | ||
expect(User.NoRowsDeletedError).to.not.be.eql(OtherUser.NoRowsDeletedError); | ||
expect(err).to.not.be.an.instanceof(OtherUser.NoRowsDeletedError); | ||
}); | ||
}); | ||
}); | ||
@@ -100,13 +143,2 @@ | ||
describe('get', function() { | ||
it('should use the same get method as the Backbone library', function() { | ||
var attached = ['get']; | ||
_.each(attached, function(item) { | ||
deepEqual(bookshelf.Model.prototype[item], Backbone.Model.prototype[item]); | ||
}); | ||
}); | ||
}); | ||
describe('query', function() { | ||
@@ -233,3 +265,3 @@ | ||
return _.reduce(attrs, function(memo, val, key) { | ||
memo[_.str.underscored(key)] = val; | ||
memo[_.snakeCase(key)] = val; | ||
return memo; | ||
@@ -290,3 +322,5 @@ }, {}); | ||
}); | ||
return expect(model.fetch()).to.be.rejected; | ||
return model.fetch().throw(new Error('Err')).catch(function(err) { | ||
assert(err.message === 'This failed') | ||
}) | ||
}); | ||
@@ -297,3 +331,3 @@ | ||
model.on('fetching', function(model, columns, options) { | ||
expect(options.query.whereIn).to.be.a.function; | ||
assert(typeof options.query.whereIn === 'function') | ||
}); | ||
@@ -308,4 +342,3 @@ return model.fetch(); | ||
}); | ||
return expect(model.fetch()).to.be.fulfilled; | ||
return model.fetch() | ||
}); | ||
@@ -361,3 +394,3 @@ | ||
}, function(err) { | ||
expect(err.message).to.equal('No rows were affected in the update, did you mean to pass the {method: "insert"} option?'); | ||
expect(err.message).to.equal('No Rows Updated'); | ||
}); | ||
@@ -367,2 +400,6 @@ | ||
it('does not error if if the row was not updated but require is false', function() { | ||
return new Site({id: 200, name: 'This doesnt exist'}).save({}, {require: false}); | ||
}); | ||
it('should not error if updated row was not affected', function() { | ||
@@ -532,3 +569,3 @@ return new Site({id: 5, name: 'Fifth site, explicity created'}).save(); | ||
m.on('destroying', function(model, options) { | ||
expect(options.query.whereIn).to.be.a.function; | ||
assert(typeof options.query.whereIn === "function"); | ||
}); | ||
@@ -538,2 +575,8 @@ return m.destroy(); | ||
it('will throw an error when trying to destroy a non-existent object with {require: true}', function() { | ||
return new Site({id: 1337}).destroy({require: true}).catch(function(err) { | ||
assert(err instanceof bookshelf.NoRowsDeletedError) | ||
}) | ||
}); | ||
}); | ||
@@ -714,5 +757,2 @@ | ||
var model = new Models.Site({id: 1}); | ||
model.on('change', function() { | ||
count++; | ||
}); | ||
equal(model.previous('id'), void 0); | ||
@@ -727,3 +767,3 @@ | ||
model.set('id', 1); | ||
equal(count, 1); | ||
deepEqual(model.changed, {}); | ||
}); | ||
@@ -740,3 +780,3 @@ | ||
return new Models.Site({id: 1}).fetch().then(function(site) { | ||
expect(site.hasChanged()).to.be.false; | ||
equal(site.hasChanged(), false); | ||
site.set('name', 'Changed site'); | ||
@@ -743,0 +783,0 @@ equal(site.hasChanged('name'), true); |
var _ = require('lodash'); | ||
var equal = require('assert').equal; | ||
var deepEqual = require('assert').deepEqual; | ||
var expect = require('chai').expect; | ||
@@ -48,2 +49,18 @@ module.exports = function (bookshelf) { | ||
it('defaults virtual properties with no setter to a noop', function () { | ||
var m = new (bookshelf.Model.extend({ | ||
virtuals: { | ||
fullName: function () { | ||
return this.get('firstName') + ' ' + this.get('lastName'); | ||
} | ||
} | ||
}))({fullName: 'Jane Doe'}); | ||
expect(m.attributes.fullName).to.be.undefined; | ||
m.set('fullName', 'John Doe'); | ||
expect(m.attributes.fullName).to.be.undefined; | ||
}); | ||
it('allows virtual properties to be set and get like normal properties', function () { | ||
@@ -50,0 +67,0 @@ var m = new (bookshelf.Model.extend({ |
@@ -45,2 +45,6 @@ var _ = require('lodash'); | ||
var LeftModel = Models.LeftModel; | ||
var RightModel = Models.RightModel; | ||
var JoinModel = Models.JoinModel; | ||
describe('Bookshelf Relations', function() { | ||
@@ -220,4 +224,5 @@ | ||
}); | ||
expect(site1.related('admins').attach(admin1)).to.be.rejected; | ||
return site1.related('admins').attach(admin1); | ||
}).throw(new Error()).catch(function(err) { | ||
equal(err.message, 'This failed') | ||
}); | ||
@@ -237,3 +242,5 @@ }); | ||
}).then(function() { | ||
expect(site1.related('admins').detach(admin1)).to.be.rejected; | ||
return site1.related('admins').detach(admin1) | ||
}).throw(new Error()).catch(function(err) { | ||
equal(err.message, 'This failed') | ||
}); | ||
@@ -270,4 +277,6 @@ }); | ||
]); | ||
}) | ||
.then(function(resp) { | ||
}).spread(function(site1Admins, site2Admins) { | ||
expect(site1Admins).to.equal(site1.related('admins')); | ||
expect(site2Admins).to.equal(site2.related('admins')); | ||
expect(site1.related('admins')).to.have.length(2); | ||
@@ -452,4 +461,3 @@ expect(site2.related('admins')).to.have.length(1); | ||
}).catch(function (err) { | ||
expect(err).to.be.ok; | ||
expect(err).to.be.an.instanceof(Error); | ||
assert(err instanceof Error); | ||
}); | ||
@@ -868,4 +876,83 @@ }); | ||
describe('Issue #578 - lifecycle events on pivot model for belongsToMany().through()', function () { | ||
// Overrides the `initialize` method on the JoinModel to throw an Error | ||
// when the current lifecycleEvent is triggered. Additionally overrides | ||
// the Left/Right models `.belongsToMany().through()` configuration with | ||
// the overridden JoinModel. | ||
function initializeModelsForLifecycleEvent(lifecycleEvent) { | ||
JoinModel = JoinModel.extend({ | ||
initialize: (function (v) { | ||
return function () { | ||
this.on(lifecycleEvent, function () { | ||
throw new Error('`' + lifecycleEvent + '` triggered on JoinModel()'); | ||
}); | ||
}; | ||
}(lifecycleEvent)) | ||
}); | ||
LeftModel = LeftModel.extend({ | ||
rights: function () { | ||
return this.belongsToMany(RightModel).through(JoinModel); | ||
} | ||
}); | ||
RightModel = RightModel.extend({ | ||
lefts: function () { | ||
return this.belongsToMany(LeftModel).through(JoinModel).withPivot(['parsedName']); | ||
} | ||
}); | ||
}; | ||
// First, initialize the models for the current `lifecycleEvent`, then | ||
// step through the entire lifecycle of a JoinModel, returning a promise. | ||
function joinModelLifecycleRoutine(lifecycleEvent) { | ||
initializeModelsForLifecycleEvent(lifecycleEvent); | ||
return (new LeftModel).save().then(function (left) { | ||
// creating, saving, created, saved | ||
return [left, left.rights().create()]; | ||
}).spread(function (left, right) { | ||
// fetching, fetched | ||
return [left, right, right.lefts().fetch()]; | ||
}).spread(function (left, right, lefts) { | ||
// updating, updated | ||
return [left, right, left.rights().updatePivot({})]; | ||
}).spread(function (left, right, relationship) { | ||
return (new LeftModel).save().then(function (left) { | ||
return [left, right, right.lefts().attach(left)]; | ||
}); | ||
}).spread(function (left, right, relationship) { | ||
// destroying, destroyed | ||
return left.rights().detach(right); | ||
}); | ||
} | ||
// For each lifecycle event that should be triggered on the JoinModel, | ||
// build a test that verifies the expected Error is being thrown by the | ||
// JoinModel's lifecycle event handler. | ||
[ | ||
'creating', | ||
'created', | ||
'saving', | ||
'saved', | ||
'fetching', | ||
'fetched', | ||
'updating', | ||
'updated', | ||
'destroying', | ||
'destroyed' | ||
].forEach(function (v) { | ||
it('should trigger pivot model lifecycle event: ' + v, function () { | ||
return joinModelLifecycleRoutine(v).catch(function (err) { | ||
assert(err instanceof Error) | ||
equal(err.message, '`' + v + '` triggered on JoinModel()'); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
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
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
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
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 2 instances 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
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
7
11
59
53
14
1
511708
13927
+ Addedchalk@^1.0.0
+ Addedansi-regex@2.1.1(transitive)
+ Addedansi-styles@2.2.1(transitive)
+ Addedchalk@1.1.3(transitive)
+ Addedescape-string-regexp@1.0.5(transitive)
+ Addedhas-ansi@2.0.0(transitive)
+ Addedlodash@3.10.1(transitive)
+ Addedstrip-ansi@3.0.1(transitive)
+ Addedsupports-color@2.0.0(transitive)
- Removedbackbone@1.1.0
- Removedsimple-extend@0.1.0
- Removedtrigger-then@0.3.x
- Removedbackbone@1.1.0(transitive)
- Removedlodash@2.4.2(transitive)
- Removedsimple-extend@0.1.0(transitive)
- Removedtrigger-then@0.3.0(transitive)
- Removedunderscore@1.13.7(transitive)
Updatedbluebird@^2.9.4
Updatedlodash@^3.7.0
Updatedsemver@^4.3.3