Comparing version 0.6.1 to 0.6.2
183
bookshelf.js
@@ -1,2 +0,2 @@ | ||
// Bookshelf.js 0.6.1 | ||
// Bookshelf.js 0.6.2 | ||
// --------------- | ||
@@ -8,117 +8,106 @@ | ||
// http://bookshelfjs.org | ||
(function(define) { | ||
"use strict"; | ||
// All external libraries needed in this scope. | ||
var _ = require('lodash'); | ||
var Knex = require('knex'); | ||
define(function(require, exports, module) { | ||
// All local dependencies... These are the main objects that | ||
// need to be augmented in the constructor to work properly. | ||
var SqlModel = require('./dialects/sql/model').Model; | ||
var SqlCollection = require('./dialects/sql/collection').Collection; | ||
var SqlRelation = require('./dialects/sql/relation').Relation; | ||
// All external libraries needed in this scope. | ||
var _ = require('lodash'); | ||
var Knex = require('knex'); | ||
// Finally, the `Events`, which we've supplemented 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('./dialects/base/events').Events; | ||
// All local dependencies... These are the main objects that | ||
// need to be augmented in the constructor to work properly. | ||
var SqlModel = require('./dialects/sql/model').Model; | ||
var SqlCollection = require('./dialects/sql/collection').Collection; | ||
var SqlRelation = require('./dialects/sql/relation').Relation; | ||
// 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. | ||
var Bookshelf = function(knex) { | ||
// Finally, the `Events`, which we've supplemented 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('./dialects/base/events').Events; | ||
// Allows you to construct the library with either `Bookshelf(opts)` | ||
// or `new Bookshelf(opts)`. | ||
if (!(this instanceof Bookshelf)) { | ||
return new Bookshelf(knex); | ||
} | ||
// 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. | ||
var Bookshelf = function(knex) { | ||
// 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. | ||
if (!knex.client || !(knex.client instanceof Knex.ClientBase)) { | ||
knex = new Knex(knex); | ||
} | ||
// Allows you to construct the library with either `Bookshelf(opts)` | ||
// or `new Bookshelf(opts)`. | ||
if (!(this instanceof Bookshelf)) { | ||
return new Bookshelf(knex); | ||
// 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. | ||
var ModelCtor = this.Model = SqlModel.extend({ | ||
_builder: function(tableName) { | ||
return knex(tableName); | ||
}, | ||
_relation: function(type, Target, options) { | ||
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. | ||
if (!knex.client || !(knex.client instanceof Knex.ClientBase)) { | ||
knex = new Knex(knex); | ||
// The collection also references the correct `Model`, specified above, for creating | ||
// new `Model` instances in the collection. We also extend with the correct builder / | ||
// `knex` combo. | ||
var CollectionCtor = this.Collection = SqlCollection.extend({ | ||
model: ModelCtor, | ||
_builder: function(tableName) { | ||
return knex(tableName); | ||
} | ||
}); | ||
// 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. | ||
var ModelCtor = this.Model = SqlModel.extend({ | ||
_builder: function(tableName) { | ||
return knex(tableName); | ||
}, | ||
_relation: function(type, Target, options) { | ||
return new Relation(type, Target, options); | ||
} | ||
}); | ||
// Used internally, the `Relation` helps in simplifying the relationship building, | ||
// centralizing all logic dealing with type & option handling. | ||
var Relation = Bookshelf.Relation = SqlRelation.extend({ | ||
Model: ModelCtor, | ||
Collection: CollectionCtor | ||
}); | ||
// The collection also references the correct `Model`, specified above, for creating | ||
// new `Model` instances in the collection. We also extend with the correct builder / | ||
// `knex` combo. | ||
var CollectionCtor = this.Collection = SqlCollection.extend({ | ||
model: ModelCtor, | ||
_builder: function(tableName) { | ||
return knex(tableName); | ||
} | ||
}); | ||
// Grab a reference to the `knex` instance passed (or created) in this constructor, | ||
// for convenience. | ||
this.knex = knex; | ||
}; | ||
// Used internally, the `Relation` helps in simplifying the relationship building, | ||
// centralizing all logic dealing with type & option handling. | ||
var Relation = Bookshelf.Relation = SqlRelation.extend({ | ||
Model: ModelCtor, | ||
Collection: CollectionCtor | ||
}); | ||
// A `Bookshelf` instance may be used as a top-level pub-sub bus, as it mixes in the | ||
// `Events` object. It also contains the version number, and a `Transaction` method | ||
// referencing the correct version of `knex` passed into the object. | ||
_.extend(Bookshelf.prototype, Events, { | ||
// Grab a reference to the `knex` instance passed (or created) in this constructor, | ||
// for convenience. | ||
this.knex = knex; | ||
}; | ||
// Keep in sync with `package.json`. | ||
VERSION: '0.6.2', | ||
// A `Bookshelf` instance may be used as a top-level pub-sub bus, as it mixes in the | ||
// `Events` object. It also contains the version number, and a `Transaction` method | ||
// referencing the correct version of `knex` passed into the object. | ||
_.extend(Bookshelf.prototype, Events, { | ||
// Helper method to wrap a series of Bookshelf actions in a `knex` transaction block; | ||
transaction: function() { | ||
return this.knex.transaction.apply(this, arguments); | ||
}, | ||
// Keep in sync with `package.json`. | ||
VERSION: '0.6.1', | ||
// Provides a nice, tested, standardized way of adding plugins to a `Bookshelf` instance, | ||
// injecting the current instance into the plugin, which should be a module.exports. | ||
plugin: function(plugin) { | ||
plugin(this); | ||
return this; | ||
} | ||
// Helper method to wrap a series of Bookshelf actions in a `knex` transaction block; | ||
transaction: function() { | ||
return this.knex.transaction.apply(this, arguments); | ||
}, | ||
}); | ||
// Provides a nice, tested, standardized way of adding plugins to a `Bookshelf` instance, | ||
// injecting the current instance into the plugin, which should be a module.exports. | ||
plugin: function(plugin) { | ||
plugin(this); | ||
return this; | ||
} | ||
// Alias to `new Bookshelf(opts)`. | ||
Bookshelf.initialize = function(knex) { | ||
return new this(knex); | ||
}; | ||
}); | ||
// The `forge` function properly instantiates a new Model or Collection | ||
// without needing the `new` operator... to make object creation cleaner | ||
// and more chainable. | ||
SqlModel.forge = SqlCollection.forge = function() { | ||
var inst = Object.create(this.prototype); | ||
var obj = this.apply(inst, arguments); | ||
return (Object(obj) === obj ? obj : inst); | ||
}; | ||
// Alias to `new Bookshelf(opts)`. | ||
Bookshelf.initialize = function(knex) { | ||
return new this(knex); | ||
}; | ||
// The `forge` function properly instantiates a new Model or Collection | ||
// without needing the `new` operator... to make object creation cleaner | ||
// and more chainable. | ||
SqlModel.forge = SqlCollection.forge = function() { | ||
var inst = Object.create(this.prototype); | ||
var obj = this.apply(inst, arguments); | ||
return (Object(obj) === obj ? obj : inst); | ||
}; | ||
// Finally, export `Bookshelf` to the world. | ||
module.exports = Bookshelf; | ||
}); | ||
})( | ||
typeof define === 'function' && define.amd ? define : function (factory) { factory(require, exports, module); } | ||
); | ||
// Finally, export `Bookshelf` to the world. | ||
module.exports = Bookshelf; |
// Base Collection | ||
// --------------- | ||
(function(define) { | ||
"use strict"; | ||
// All exernal dependencies required in this scope. | ||
var _ = require('lodash'); | ||
var Backbone = require('backbone'); | ||
// The `CollectionBase` is an object that takes | ||
define(function(require, exports) { | ||
// All components that need to be referenced in this scope. | ||
var Events = require('./events').Events; | ||
var Promise = require('./promise').Promise; | ||
var ModelBase = require('./model').ModelBase; | ||
// All exernal dependencies required in this scope. | ||
var _ = require('lodash'); | ||
var Backbone = require('backbone'); | ||
var array = []; | ||
var push = array.push; | ||
var splice = array.splice; | ||
// All components that need to be referenced in this scope. | ||
var Events = require('./events').Events; | ||
var Promise = require('./promise').Promise; | ||
var ModelBase = require('./model').ModelBase; | ||
var CollectionBase = function(models, options) { | ||
if (options) _.extend(this, _.pick(options, collectionProps)); | ||
this._reset(); | ||
this.initialize.apply(this, arguments); | ||
if (models) this.reset(models, _.extend({silent: true}, options)); | ||
}; | ||
var array = []; | ||
var push = array.push; | ||
var splice = array.splice; | ||
// List of attributes attached directly from the constructor's options object. | ||
var collectionProps = ['model', 'comparator']; | ||
var CollectionBase = function(models, options) { | ||
if (options) _.extend(this, _.pick(options, collectionProps)); | ||
this._reset(); | ||
this.initialize.apply(this, arguments); | ||
if (models) this.reset(models, _.extend({silent: true}, options)); | ||
}; | ||
// 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']; | ||
// List of attributes attached directly from the constructor's options object. | ||
var collectionProps = ['model', 'comparator']; | ||
// Copied over from Backbone. | ||
var setOptions = {add: true, remove: true, merge: true}; | ||
// 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']; | ||
_.extend(CollectionBase.prototype, _.omit(Backbone.Collection.prototype, collectionOmitted), Events, { | ||
// Copied over from Backbone. | ||
var setOptions = {add: true, remove: true, merge: true}; | ||
// The `tableName` on the associated Model, used in relation building. | ||
tableName: function() { | ||
return _.result(this.model.prototype, 'tableName'); | ||
}, | ||
_.extend(CollectionBase.prototype, _.omit(Backbone.Collection.prototype, collectionOmitted), Events, { | ||
// The `idAttribute` on the associated Model, used in relation building. | ||
idAttribute: function() { | ||
return this.model.prototype.idAttribute; | ||
}, | ||
// The `tableName` on the associated Model, used in relation building. | ||
tableName: function() { | ||
return _.result(this.model.prototype, 'tableName'); | ||
}, | ||
// 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 `idAttribute` on the associated Model, used in relation building. | ||
idAttribute: function() { | ||
return this.model.prototype.idAttribute; | ||
}, | ||
// 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. | ||
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; | ||
continue; | ||
} | ||
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; | ||
continue; | ||
} | ||
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)); | ||
} else { | ||
if (order) this.models.length = 0; | ||
push.apply(this.models, order || 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; | ||
push.apply(this.models, order || toAdd); | ||
} | ||
} | ||
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); | ||
} | ||
return this; | ||
}, | ||
// 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. | ||
_prepareModel: function(attrs, options) { | ||
if (attrs instanceof ModelBase) return attrs; | ||
return new this.model(attrs, options); | ||
}, | ||
// 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); | ||
}, | ||
// Convenience method for map, returning a `Promise.all` promise. | ||
mapThen: function(iterator, context) { | ||
return Promise.all(this.map(iterator, context)); | ||
}, | ||
// Convenience method for map, returning a `Promise.all` promise. | ||
mapThen: function(iterator, context) { | ||
return Promise.all(this.map(iterator, context)); | ||
}, | ||
// Convenience method for invoke, returning a `Promise.all` promise. | ||
invokeThen: function() { | ||
return Promise.all(this.invoke.apply(this, arguments)); | ||
}, | ||
// Convenience method for invoke, returning a `Promise.all` promise. | ||
invokeThen: function() { | ||
return Promise.all(this.invoke.apply(this, arguments)); | ||
}, | ||
fetch: function() { | ||
return Promise.rejected('The fetch method has not been implemented'); | ||
}, | ||
fetch: function() { | ||
return Promise.rejected('The fetch method has not been implemented'); | ||
} | ||
_handleResponse: function() {}, | ||
}); | ||
_handleEager: function() {} | ||
// List of attributes attached directly from the `options` passed to the constructor. | ||
var modelProps = ['tableName', 'hasTimestamps']; | ||
}); | ||
CollectionBase.extend = Backbone.Collection.extend; | ||
// List of attributes attached directly from the `options` passed to the constructor. | ||
var modelProps = ['tableName', 'hasTimestamps']; | ||
// Helper to mixin one or more additional items to the current prototype. | ||
CollectionBase.include = function() { | ||
_.extend.apply(_, [this.prototype].concat(_.toArray(arguments))); | ||
return this; | ||
}; | ||
CollectionBase.extend = Backbone.Collection.extend; | ||
// Helper to mixin one or more additional items to the current prototype. | ||
CollectionBase.include = function() { | ||
_.extend.apply(_, [this.prototype].concat(_.toArray(arguments))); | ||
return this; | ||
}; | ||
exports.CollectionBase = CollectionBase; | ||
}); | ||
})( | ||
typeof define === 'function' && define.amd ? define : function (factory) { factory(require, exports); } | ||
); | ||
exports.CollectionBase = CollectionBase; |
// Eager Base | ||
// --------------- | ||
(function(define) { | ||
"use strict"; | ||
// The EagerBase provides a scaffold for handling with eager relation | ||
@@ -11,107 +8,102 @@ // pairing, by queueing the appropriate related method calls with | ||
// `pushModels` for pairing the models depending on the database need. | ||
define(function(require, exports) { | ||
var _ = require('lodash'); | ||
var Backbone = require('backbone'); | ||
var Promise = require('./promise').Promise; | ||
var _ = require('lodash'); | ||
var Backbone = require('backbone'); | ||
var Promise = require('./promise').Promise; | ||
var EagerBase = function(parent, parentResponse, target) { | ||
this.parent = parent; | ||
this.target = target; | ||
this.parentResponse = parentResponse; | ||
}; | ||
var EagerBase = function(parent, parentResponse, target) { | ||
this.parent = parent; | ||
this.parentResponse = parentResponse; | ||
this.target = target; | ||
}; | ||
EagerBase.prototype = { | ||
EagerBase.prototype = { | ||
// This helper function is used internally to determine which relations | ||
// are necessary for fetching based on the `model.load` or `withRelated` option. | ||
fetch: Promise.method(function(options) { | ||
var relationName, related, relation; | ||
var target = this.target; | ||
var handled = this.handled = {}; | ||
var withRelated = this.prepWithRelated(options.withRelated); | ||
var subRelated = {}; | ||
// This helper function is used internally to determine which relations | ||
// are necessary for fetching based on the `model.load` or `withRelated` option. | ||
fetch: Promise.method(function(options) { | ||
var relationName, related, relation; | ||
var target = this.target; | ||
var handled = this.handled = {}; | ||
var withRelated = this.prepWithRelated(options.withRelated); | ||
var subRelated = {}; | ||
// Internal flag to determine whether to set the ctor(s) on the `Relation` object. | ||
target._isEager = true; | ||
// Internal flag to determine whether to set the ctor(s) on the `Relation` object. | ||
target._isEager = true; | ||
// Eager load each of the `withRelated` relation item, splitting on '.' | ||
// which indicates a nested eager load. | ||
for (var key in withRelated) { | ||
// Eager load each of the `withRelated` relation item, splitting on '.' | ||
// which indicates a nested eager load. | ||
for (var key in withRelated) { | ||
related = key.split('.'); | ||
relationName = related[0]; | ||
related = key.split('.'); | ||
relationName = related[0]; | ||
// Add additional eager items to an array, to load at the next level in the query. | ||
if (related.length > 1) { | ||
var relatedObj = {}; | ||
subRelated[relationName] || (subRelated[relationName] = []); | ||
relatedObj[related.slice(1).join('.')] = withRelated[key]; | ||
subRelated[relationName].push(relatedObj); | ||
} | ||
// Add additional eager items to an array, to load at the next level in the query. | ||
if (related.length > 1) { | ||
var relatedObj = {}; | ||
subRelated[relationName] || (subRelated[relationName] = []); | ||
relatedObj[related.slice(1).join('.')] = withRelated[key]; | ||
subRelated[relationName].push(relatedObj); | ||
} | ||
// Only allow one of a certain nested type per-level. | ||
if (handled[relationName]) continue; | ||
// Only allow one of a certain nested type per-level. | ||
if (handled[relationName]) continue; | ||
relation = target[relationName](); | ||
relation = target[relationName](); | ||
if (!relation) throw new Error(relationName + ' is not defined on the model.'); | ||
if (!relation) throw new Error(relationName + ' is not defined on the model.'); | ||
handled[relationName] = relation; | ||
} | ||
handled[relationName] = relation; | ||
} | ||
// Delete the internal flag from the model. | ||
delete target._isEager; | ||
// Delete the internal flag from the model. | ||
delete target._isEager; | ||
// Fetch all eager loaded models, loading them onto | ||
// an array of pending deferred objects, which will handle | ||
// all necessary pairing with parent objects, etc. | ||
var pendingDeferred = []; | ||
for (relationName in handled) { | ||
pendingDeferred.push(this.eagerFetch(relationName, handled[relationName], _.extend({}, options, { | ||
isEager: true, | ||
withRelated: subRelated[relationName], | ||
beforeFn: withRelated[relationName] || noop | ||
}))); | ||
} | ||
// Fetch all eager loaded models, loading them onto | ||
// an array of pending deferred objects, which will handle | ||
// all necessary pairing with parent objects, etc. | ||
var pendingDeferred = []; | ||
for (relationName in handled) { | ||
pendingDeferred.push(this.eagerFetch(relationName, handled[relationName], _.extend({}, options, { | ||
isEager: true, | ||
withRelated: subRelated[relationName], | ||
beforeFn: withRelated[relationName] || noop | ||
}))); | ||
} | ||
// Return a deferred handler for all of the nested object sync | ||
// returning the original response when these syncs & pairings are complete. | ||
return Promise.all(pendingDeferred).yield(this.parentResponse); | ||
}), | ||
// Return a deferred handler for all of the nested object sync | ||
// returning the original response when these syncs & pairings are complete. | ||
return Promise.all(pendingDeferred).yield(this.parentResponse); | ||
}), | ||
// Prep the `withRelated` object, to normalize into an object where each | ||
// has a function that is called when running the query. | ||
prepWithRelated: function(withRelated) { | ||
if (!_.isArray(withRelated)) withRelated = [withRelated]; | ||
return _.reduce(withRelated, function(memo, item) { | ||
_.isString(item) ? memo[item] = noop : _.extend(memo, item); | ||
return memo; | ||
}, {}); | ||
}, | ||
// Prep the `withRelated` object, to normalize into an object where each | ||
// has a function that is called when running the query. | ||
prepWithRelated: function(withRelated) { | ||
if (!_.isArray(withRelated)) withRelated = [withRelated]; | ||
var obj = {}; | ||
for (var i = 0, l = withRelated.length; i < l; i++) { | ||
var related = withRelated[i]; | ||
_.isString(related) ? obj[related] = noop : _.extend(obj, related); | ||
} | ||
return obj; | ||
}, | ||
// Pushes each of the incoming models onto a new `related` array, | ||
// which is used to correcly pair additional nested relations. | ||
pushModels: function(relationName, handled, resp) { | ||
var models = this.parent; | ||
var relatedData = handled.relatedData; | ||
var related = []; | ||
for (var i = 0, l = resp.length; i < l; i++) { | ||
related.push(relatedData.createModel(resp[i])); | ||
} | ||
return relatedData.eagerPair(relationName, related, models); | ||
// Pushes each of the incoming models onto a new `related` array, | ||
// which is used to correcly pair additional nested relations. | ||
pushModels: function(relationName, handled, resp) { | ||
var models = this.parent; | ||
var relatedData = handled.relatedData; | ||
var related = []; | ||
for (var i = 0, l = resp.length; i < l; i++) { | ||
related.push(relatedData.createModel(resp[i])); | ||
} | ||
return relatedData.eagerPair(relationName, related, models); | ||
} | ||
}; | ||
}; | ||
var noop = function() {}; | ||
var noop = function() {}; | ||
EagerBase.extend = Backbone.Model.extend; | ||
EagerBase.extend = Backbone.Model.extend; | ||
exports.EagerBase = EagerBase; | ||
}); | ||
})( | ||
typeof define === 'function' && define.amd ? define : function (factory) { factory(require, exports); } | ||
); | ||
exports.EagerBase = EagerBase; |
// Events | ||
// --------------- | ||
(function(define) { | ||
"use strict"; | ||
var Promise = require('./promise').Promise; | ||
var Backbone = require('backbone'); | ||
var triggerThen = require('trigger-then'); | ||
define(function(require, exports) { | ||
// Mixin the `triggerThen` function into all relevant Backbone objects, | ||
// so we can have event driven async validations, functions, etc. | ||
triggerThen(Backbone, Promise); | ||
var Promise = require('./promise').Promise; | ||
var Backbone = require('backbone'); | ||
var triggerThen = require('trigger-then'); | ||
// Mixin the `triggerThen` function into all relevant Backbone objects, | ||
// so we can have event driven async validations, functions, etc. | ||
triggerThen(Backbone, Promise); | ||
exports.Events = Backbone.Events; | ||
}); | ||
})( | ||
typeof define === 'function' && define.amd ? define : function(factory) { factory(require, exports); } | ||
); | ||
exports.Events = Backbone.Events; |
// Base Model | ||
// --------------- | ||
(function(define) { | ||
var _ = require('lodash'); | ||
var Backbone = require('backbone'); | ||
"use strict"; | ||
var Events = require('./events').Events; | ||
var Promise = require('./promise').Promise; | ||
define(function(require, exports) { | ||
// 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' | ||
]; | ||
var _ = require('lodash'); | ||
var Backbone = require('backbone'); | ||
// 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) { | ||
var attrs = attributes || {}; | ||
options || (options = {}); | ||
this.attributes = Object.create(null); | ||
this._reset(); | ||
this.relations = {}; | ||
this.cid = _.uniqueId('c'); | ||
if (options) { | ||
_.extend(this, _.pick(options, modelProps)); | ||
if (options.parse) attrs = this.parse(attrs, options) || {}; | ||
} | ||
this.set(attrs, options); | ||
this.initialize.apply(this, arguments); | ||
}; | ||
var Events = require('./events').Events; | ||
var Promise = require('./promise').Promise; | ||
_.extend(ModelBase.prototype, _.omit(Backbone.Model.prototype), Events, { | ||
// 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' | ||
]; | ||
// 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; | ||
// 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) { | ||
var attrs = attributes || {}; | ||
// Handle both `"key", value` and `{key: value}` -style arguments. | ||
if (typeof key === 'object') { | ||
attrs = key; | ||
options = val; | ||
} else { | ||
(attrs = {})[key] = val; | ||
} | ||
options || (options = {}); | ||
this.attributes = Object.create(null); | ||
this._reset(); | ||
this.relations = {}; | ||
this.cid = _.uniqueId('c'); | ||
if (options) { | ||
_.extend(this, _.pick(options, modelProps)); | ||
if (options.parse) attrs = this.parse(attrs, options) || {}; | ||
} | ||
this.set(attrs, options); | ||
this.initialize.apply(this, arguments); | ||
}; | ||
_.extend(ModelBase.prototype, _.omit(Backbone.Model.prototype), Events, { | ||
// Extract attributes and options. | ||
var hasChanged = false; | ||
var unset = options.unset; | ||
var current = this.attributes; | ||
var prev = this._previousAttributes; | ||
// 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; | ||
// Check for changes of `id`. | ||
if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; | ||
// Handle both `"key", value` and `{key: value}` -style arguments. | ||
if (typeof key === 'object') { | ||
attrs = key; | ||
options = val; | ||
// 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; | ||
} | ||
// Extract attributes and options. | ||
var hasChanged = false; | ||
var unset = options.unset; | ||
var current = this.attributes; | ||
var prev = this._previousAttributes; | ||
if (hasChanged && !options.silent) this.trigger('change', this, options); | ||
return this; | ||
}, | ||
// 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`. | ||
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() : 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; | ||
}, | ||
// **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 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`. | ||
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() : relation; | ||
} | ||
if (this.pivot) { | ||
var pivot = this.pivot.attributes; | ||
for (key in pivot) { | ||
attrs['_pivot_' + key] = pivot[key]; | ||
} | ||
} | ||
return attrs; | ||
}, | ||
// **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. | ||
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); | ||
}, | ||
// **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; | ||
}, | ||
// 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; | ||
}, | ||
// 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); | ||
}, | ||
// 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 = {}; | ||
vals[keys[1]] = d; | ||
if (this.isNew(options) && (!options || options.method !== 'update')) vals[keys[0]] = d; | ||
return vals; | ||
}, | ||
// 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; | ||
}, | ||
// 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; | ||
}, | ||
// 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 = {}; | ||
vals[keys[1]] = d; | ||
if (this.isNew(options) && (!options || options.method !== 'update')) vals[keys[0]] = d; | ||
return vals; | ||
}, | ||
fetch: function() {}, | ||
// 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; | ||
}, | ||
save: function() {}, | ||
fetch: function() {}, | ||
// 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) : {}; | ||
return Promise.bind(this).then(function() { | ||
return this.triggerThen('destroying', this, options); | ||
}).then(function() { | ||
return this.sync(options).del(); | ||
}).then(function(resp) { | ||
this.clear(); | ||
return this.triggerThen('destroyed', this, resp, options); | ||
}).then(this._reset); | ||
}) | ||
save: function() {}, | ||
}); | ||
// 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) : {}; | ||
return Promise.bind(this).then(function() { | ||
return this.triggerThen('destroying', this, options); | ||
}).then(function() { | ||
return this.sync(options).del(); | ||
}).then(function(resp) { | ||
this.clear(); | ||
return this.triggerThen('destroyed', this, resp, options); | ||
}).then(this._reset); | ||
}), | ||
// List of attributes attached directly from the `options` passed to the constructor. | ||
var modelProps = ['tableName', 'hasTimestamps']; | ||
_handleResponse: function() {}, | ||
ModelBase.extend = Backbone.Model.extend; | ||
_handleEager: function() {} | ||
// Helper to mixin one or more additional items to the current prototype. | ||
ModelBase.include = function() { | ||
_.extend.apply(_, [this.prototype].concat(_.toArray(arguments))); | ||
return this; | ||
}; | ||
}); | ||
// List of attributes attached directly from the `options` passed to the constructor. | ||
var modelProps = ['tableName', 'hasTimestamps']; | ||
ModelBase.extend = Backbone.Model.extend; | ||
// Helper to mixin one or more additional items to the current prototype. | ||
ModelBase.include = function() { | ||
_.extend.apply(_, [this.prototype].concat(_.toArray(arguments))); | ||
return this; | ||
}; | ||
exports.ModelBase = ModelBase; | ||
}); | ||
})( | ||
typeof define === 'function' && define.amd ? define : function (factory) { factory(require, exports); } | ||
); | ||
exports.ModelBase = ModelBase; |
@@ -1,31 +0,21 @@ | ||
(function(define) { | ||
"use strict"; | ||
var Promise = require('bluebird/js/main/promise')(); | ||
define(function(require, exports) { | ||
Promise.prototype.yield = function(value) { | ||
return this.then(function() { | ||
return value; | ||
}); | ||
}; | ||
var Promise = require('bluebird/js/main/promise')(); | ||
Promise.prototype.tap = function(handler) { | ||
return this.then(handler).yield(this); | ||
}; | ||
Promise.prototype.yield = function(value) { | ||
return this.then(function() { | ||
return value; | ||
}); | ||
}; | ||
Promise.prototype.ensure = Promise.prototype.lastly; | ||
Promise.prototype.otherwise = Promise.prototype.caught; | ||
Promise.prototype.exec = Promise.prototype.nodeify; | ||
Promise.prototype.tap = function(handler) { | ||
return this.then(handler).yield(this); | ||
}; | ||
Promise.resolve = Promise.fulfilled; | ||
Promise.reject = Promise.rejected; | ||
Promise.prototype.ensure = Promise.prototype.lastly; | ||
Promise.prototype.otherwise = Promise.prototype.caught; | ||
Promise.resolve = Promise.fulfilled; | ||
Promise.reject = Promise.rejected; | ||
exports.Promise = Promise; | ||
}); | ||
})( | ||
typeof define === 'function' && define.amd ? define : function (factory) { factory(require, exports); } | ||
); | ||
exports.Promise = Promise; |
// Base Relation | ||
// --------------- | ||
(function(define) { | ||
"use strict"; | ||
var _ = require('lodash'); | ||
var Backbone = require('backbone'); | ||
define(function(require, exports) { | ||
var CollectionBase = require('./collection').CollectionBase; | ||
var _ = require('lodash'); | ||
var Backbone = require('backbone'); | ||
// Used internally, the `Relation` helps in simplifying the relationship building, | ||
// centralizing all logic dealing with type & option handling. | ||
var RelationBase = function(type, Target, options) { | ||
this.type = type; | ||
if (this.target = Target) { | ||
this.targetTableName = _.result(Target.prototype, 'tableName'); | ||
this.targetIdAttribute = _.result(Target.prototype, 'idAttribute'); | ||
} | ||
_.extend(this, options); | ||
}; | ||
var CollectionBase = require('./collection').CollectionBase; | ||
RelationBase.prototype = { | ||
// Used internally, the `Relation` helps in simplifying the relationship building, | ||
// centralizing all logic dealing with type & option handling. | ||
var RelationBase = function(type, Target, options) { | ||
this.type = type; | ||
if (this.target = Target) { | ||
this.targetTableName = _.result(Target.prototype, 'tableName'); | ||
this.targetIdAttribute = _.result(Target.prototype, 'idAttribute'); | ||
// Creates a new relation instance, used by the `Eager` relation in | ||
// dealing with `morphTo` cases, where the same relation is targeting multiple models. | ||
instance: function(type, Target, options) { | ||
return new this.constructor(type, Target, options); | ||
}, | ||
// Creates a new, unparsed model, used internally in the eager fetch helper | ||
// methods. (Parsing may mutate information necessary for eager pairing.) | ||
createModel: function(data) { | ||
if (this.target.prototype instanceof CollectionBase) { | ||
return new this.target.prototype.model(data)._reset(); | ||
} | ||
_.extend(this, options); | ||
}; | ||
return new this.target(data)._reset(); | ||
}, | ||
RelationBase.prototype = { | ||
// Eager pair the models. | ||
eagerPair: function() {} | ||
// Creates a new relation instance, used by the `Eager` relation in | ||
// dealing with `morphTo` cases, where the same relation is targeting multiple models. | ||
instance: function(type, Target, options) { | ||
return new this.constructor(type, Target, options); | ||
}, | ||
}; | ||
// Creates a new, unparsed model, used internally in the eager fetch helper | ||
// methods. (Parsing may mutate information necessary for eager pairing.) | ||
createModel: function(data) { | ||
if (this.target.prototype instanceof CollectionBase) { | ||
return new this.target.prototype.model(data)._reset(); | ||
} | ||
return new this.target(data)._reset(); | ||
}, | ||
RelationBase.extend = Backbone.Model.extend; | ||
// Eager pair the models. | ||
eagerPair: function() {} | ||
}; | ||
RelationBase.extend = Backbone.Model.extend; | ||
exports.RelationBase = RelationBase; | ||
}); | ||
})( | ||
typeof define === 'function' && define.amd ? define : function (factory) { factory(require, exports); } | ||
); | ||
exports.RelationBase = RelationBase; |
// Base Sync | ||
// --------------- | ||
(function(define) { | ||
"use strict"; | ||
// An example "sync" object which is extended | ||
@@ -11,51 +8,44 @@ // by dialect-specific sync implementations, | ||
// agnostic "Data Mapper". | ||
define(function(require, exports) { | ||
var Promise = require('./promise').Promise; | ||
var Backbone = require('backbone'); | ||
var Promise = require('./promise').Promise; | ||
var Backbone = require('backbone'); | ||
// Used as the base of the prototype chain, | ||
// a convenient object for any `instanceof` | ||
// checks you may need. | ||
var BaseSync = function() {}; | ||
// Used as the base of the prototype chain, | ||
// a convenient object for any `instanceof` | ||
// checks you may need. | ||
var BaseSync = function() {}; | ||
BaseSync.prototype = { | ||
BaseSync.prototype = { | ||
// Return a single model object. | ||
first: function() { | ||
return Promise.fulfilled({}); | ||
}, | ||
// Return a single model object. | ||
first: function() { | ||
return Promise.fulfilled({}); | ||
}, | ||
// Select one or more models, returning an array | ||
// of data objects. | ||
select: function() { | ||
return Promise.fulfilled([]); | ||
}, | ||
// Select one or more models, returning an array | ||
// of data objects. | ||
select: function() { | ||
return Promise.fulfilled([]); | ||
}, | ||
// Insert a single row, returning an object | ||
// (typically containing an "insert id"). | ||
insert: function() { | ||
return Promise.fulfilled({}); | ||
}, | ||
// Insert a single row, returning an object | ||
// (typically containing an "insert id"). | ||
insert: function() { | ||
return Promise.fulfilled({}); | ||
}, | ||
// Update an object in the data store. | ||
update: function() { | ||
return Promise.fulfilled({}); | ||
}, | ||
// Update an object in the data store. | ||
update: function() { | ||
return Promise.fulfilled({}); | ||
}, | ||
// Delete a record from the data store. | ||
del: function() { | ||
return Promise.fulfilled({}); | ||
} | ||
// Delete a record from the data store. | ||
del: function() { | ||
return Promise.fulfilled({}); | ||
} | ||
}; | ||
}; | ||
BaseSync.extend = Backbone.Model.extend; | ||
BaseSync.extend = Backbone.Model.extend; | ||
exports.BaseSync = BaseSync; | ||
}); | ||
})( | ||
typeof define === 'function' && define.amd ? define : function (factory) { factory(require, exports); } | ||
); | ||
exports.BaseSync = BaseSync; |
// Collection | ||
// --------------- | ||
(function(define) { | ||
var _ = require('lodash'); | ||
"use strict"; | ||
var Sync = require('./sync').Sync; | ||
var Helpers = require('./helpers').Helpers; | ||
var EagerRelation = require('./eager').EagerRelation; | ||
define(function(require, exports) { | ||
var CollectionBase = require('../base/collection').CollectionBase; | ||
var Promise = require('../base/promise').Promise; | ||
var _ = require('lodash'); | ||
exports.Collection = CollectionBase.extend({ | ||
var Sync = require('./sync').Sync; | ||
var Helpers = require('./helpers').Helpers; | ||
var EagerRelation = require('./eager').EagerRelation; | ||
// 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 CollectionBase = require('../base/collection').CollectionBase; | ||
var Promise = require('../base/promise').Promise; | ||
// Fetch the models for this collection, resetting the models | ||
// for the query when they arrive. | ||
fetch: Promise.method(function(options) { | ||
options = options ? _.clone(options) : {}; | ||
var sync = this.sync(options) | ||
.select() | ||
.bind(this) | ||
.tap(function(response) { | ||
if (!response || response.length === 0) { | ||
if (options.require) throw new Error('EmptyResponse'); | ||
return Promise.reject(null); | ||
} | ||
}) | ||
exports.Collection = CollectionBase.extend({ | ||
// Now, load all of the data onto the collection as necessary. | ||
.tap(handleResponse); | ||
// 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}); | ||
}, | ||
// 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. | ||
if (options.withRelated) { | ||
sync = sync.tap(handleEager(_.omit(options, 'columns'))); | ||
} | ||
// Fetch the models for this collection, resetting the models | ||
// for the query when they arrive. | ||
fetch: Promise.method(function(options) { | ||
options = options ? _.clone(options) : {}; | ||
var sync = this.sync(options) | ||
.select() | ||
.bind(this) | ||
.tap(function(response) { | ||
if (!response || response.length === 0) { | ||
if (options.require) throw new Error('EmptyResponse'); | ||
return Promise.reject(null); | ||
} | ||
}) | ||
return sync.tap(function(response) { | ||
return this.triggerThen('fetched', this, response, options); | ||
}) | ||
.caught(function(err) { | ||
if (err !== null) throw err; | ||
this.reset([], {silent: true}); | ||
}) | ||
.yield(this); | ||
}), | ||
// Now, load all of the data onto the collection as necessary. | ||
.tap(this._handleResponse); | ||
// Fetches a single model from the collection, useful on related collections. | ||
fetchOne: Promise.method(function(options) { | ||
var model = new this.model; | ||
model._knex = this.query().clone(); | ||
if (this.relatedData) model.relatedData = this.relatedData; | ||
return model.fetch(options); | ||
}), | ||
// 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. | ||
if (options.withRelated) { | ||
sync = sync.tap(this._handleEager(_.omit(options, 'columns'))); | ||
} | ||
return sync.tap(function(response) { | ||
return this.triggerThen('fetched', this, response, options); | ||
}) | ||
.caught(function(err) { | ||
if (err !== null) throw err; | ||
this.reset([], {silent: true}); | ||
}) | ||
// 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) | ||
.yield(this); | ||
}), | ||
}), | ||
// Fetches a single model from the collection, useful on related collections. | ||
fetchOne: Promise.method(function(options) { | ||
var model = new this.model; | ||
model._knex = this.query().clone(); | ||
if (this.relatedData) model.relatedData = this.relatedData; | ||
return model.fetch(options); | ||
}), | ||
// 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) : {}; | ||
var relatedData = this.relatedData; | ||
model = this._prepareModel(model, options); | ||
// 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) | ||
.yield(this); | ||
}), | ||
// If we've already added things on the query chain, | ||
// these are likely intended for the model. | ||
if (this._knex) { | ||
model._knex = this._knex; | ||
this.resetQuery(); | ||
} | ||
// 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) : {}; | ||
var relatedData = this.relatedData; | ||
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, options); | ||
} | ||
}) | ||
.then(function() { | ||
this.add(model, options); | ||
return model; | ||
}); | ||
}), | ||
// If we've already added things on the query chain, | ||
// these are likely intended for the model. | ||
if (this._knex) { | ||
model._knex = this._knex; | ||
this.resetQuery(); | ||
} | ||
// Reset the query builder, called internally | ||
// each time a query is run. | ||
resetQuery: function() { | ||
this._knex = null; | ||
return this; | ||
}, | ||
return Helpers | ||
.saveConstraints(model, relatedData) | ||
.save(null, options) | ||
.bind(this) | ||
.then(function() { | ||
if (relatedData && (relatedData.type === 'belongsToMany' || relatedData.isThrough())) { | ||
return this.attach(model, options); | ||
} | ||
}) | ||
.then(function() { | ||
this.add(model, options); | ||
return model; | ||
}); | ||
}), | ||
// Returns an instance of the query builder. | ||
query: function() { | ||
return Helpers.query(this, _.toArray(arguments)); | ||
}, | ||
// Reset the query builder, called internally | ||
// each time a query is run. | ||
resetQuery: function() { | ||
this._knex = null; | ||
return this; | ||
}, | ||
// Creates and returns a new `Bookshelf.Sync` instance. | ||
sync: function(options) { | ||
return new Sync(this, options); | ||
} | ||
// Returns an instance of the query builder. | ||
query: function() { | ||
return Helpers.query(this, _.toArray(arguments)); | ||
}, | ||
}); | ||
// Creates and returns a new `Bookshelf.Sync` instance. | ||
sync: function(options) { | ||
return new Sync(this, options); | ||
}, | ||
// Handles the response data for the collection, returning from the collection's fetch call. | ||
function handleResponse(response) { | ||
var relatedData = this.relatedData; | ||
this.set(response, {silent: true, parse: true}).invoke('_reset'); | ||
if (relatedData && relatedData.isJoined()) { | ||
relatedData.parsePivot(this.models); | ||
} | ||
} | ||
// Handles the response data for the collection, returning from the collection's fetch call. | ||
_handleResponse: function(response) { | ||
var relatedData = this.relatedData; | ||
this.set(response, {silent: true, parse: true}).invoke('_reset'); | ||
if (relatedData && relatedData.isJoined()) { | ||
relatedData.parsePivot(this.models); | ||
} | ||
}, | ||
// Handle the related data loading on the collection. | ||
_handleEager: function(options) { | ||
return function(response) { | ||
return new EagerRelation(this.models, response, new this.model()).fetch(options); | ||
}; | ||
} | ||
}); | ||
}); | ||
})( | ||
typeof define === 'function' && define.amd ? define : function (factory) { factory(require, exports); } | ||
); | ||
// Handle the related data loading on the collection. | ||
function handleEager(options) { | ||
return function(response) { | ||
return new EagerRelation(this.models, response, new this.model()).fetch(options); | ||
}; | ||
} |
// EagerRelation | ||
// --------------- | ||
(function(define) { | ||
var _ = require('lodash'); | ||
"use strict"; | ||
var Helpers = require('./helpers').Helpers; | ||
var EagerBase = require('../base/eager').EagerBase; | ||
var Promise = require('../base/promise').Promise; | ||
define(function(require, exports) { | ||
// An `EagerRelation` object temporarily stores the models from an eager load, | ||
// and handles matching eager loaded objects with their parent(s). The `tempModel` | ||
// is only used to retrieve the value of the relation method, to know the constrains | ||
// for the eager query. | ||
var EagerRelation = exports.EagerRelation = EagerBase.extend({ | ||
var _ = require('lodash'); | ||
// Handles an eager loaded fetch, passing the name of the item we're fetching for, | ||
// and any options needed for the current fetch. | ||
eagerFetch: Promise.method(function(relationName, handled, options) { | ||
var relatedData = handled.relatedData; | ||
var Helpers = require('./helpers').Helpers; | ||
var EagerBase = require('../base/eager').EagerBase; | ||
var Promise = require('../base/promise').Promise; | ||
if (relatedData.type === 'morphTo') return this.morphToFetch(relationName, relatedData, options); | ||
// An `EagerRelation` object temporarily stores the models from an eager load, | ||
// and handles matching eager loaded objects with their parent(s). The `tempModel` | ||
// is only used to retrieve the value of the relation method, to know the constrains | ||
// for the eager query. | ||
var EagerRelation = exports.EagerRelation = EagerBase.extend({ | ||
// Call the function, if one exists, to constrain the eager loaded query. | ||
options.beforeFn.call(handled, handled.query()); | ||
// Handles an eager loaded fetch, passing the name of the item we're fetching for, | ||
// and any options needed for the current fetch. | ||
eagerFetch: Promise.method(function(relationName, handled, options) { | ||
var relatedData = handled.relatedData; | ||
return handled | ||
.sync(_.extend({}, options, {parentResponse: this.parentResponse})) | ||
.select() | ||
.tap(eagerLoadHelper(this, relationName, handled, options)); | ||
}), | ||
if (relatedData.type === 'morphTo') return this.morphToFetch(relationName, relatedData, options); | ||
// Call the function, if one exists, to constrain the eager loaded query. | ||
options.beforeFn.call(handled, handled.query()); | ||
return handled | ||
.sync(_.extend({}, options, {parentResponse: this.parentResponse})) | ||
// Special handler for the eager loaded morph-to relations, this handles | ||
// the fact that there are several potential models that we need to be fetching against. | ||
// pairing them up onto a single response for the eager loading. | ||
morphToFetch: Promise.method(function(relationName, relatedData, options) { | ||
var pending = []; | ||
var groups = _.groupBy(this.parent, function(m) { | ||
return m.get(relatedData.morphName + '_type'); | ||
}); | ||
for (var group in groups) { | ||
var Target = Helpers.morphCandidate(relatedData.candidates, group); | ||
var target = new Target(); | ||
pending.push(target | ||
.query('whereIn', | ||
_.result(target, 'idAttribute'), | ||
_.uniq(_.invoke(groups[group], 'get', relatedData.morphName + '_id')) | ||
) | ||
.sync(options) | ||
.select() | ||
.tap(eagerLoadHelper(this, relationName, handled, options)); | ||
}), | ||
.tap(eagerLoadHelper(this, relationName, { | ||
relatedData: relatedData.instance('morphTo', Target, {morphName: relatedData.morphName}) | ||
}, options))); | ||
} | ||
return Promise.all(pending).then(function(resps) { | ||
return _.flatten(resps); | ||
}); | ||
}) | ||
// Special handler for the eager loaded morph-to relations, this handles | ||
// the fact that there are several potential models that we need to be fetching against. | ||
// pairing them up onto a single response for the eager loading. | ||
morphToFetch: Promise.method(function(relationName, relatedData, options) { | ||
var pending = []; | ||
var groups = _.groupBy(this.parent, function(m) { | ||
return m.get(relatedData.morphName + '_type'); | ||
}); | ||
for (var group in groups) { | ||
var Target = Helpers.morphCandidate(relatedData.candidates, group); | ||
var target = new Target(); | ||
pending.push(target | ||
.query('whereIn', | ||
_.result(target, 'idAttribute'), | ||
_.uniq(_.invoke(groups[group], 'get', relatedData.morphName + '_id')) | ||
) | ||
.sync(options) | ||
.select() | ||
.tap(eagerLoadHelper(this, relationName, { | ||
relatedData: relatedData.instance('morphTo', Target, {morphName: relatedData.morphName}) | ||
}, options))); | ||
} | ||
return Promise.all(pending).then(function(resps) { | ||
return _.flatten(resps); | ||
}); | ||
}) | ||
}); | ||
}); | ||
// Handles the eager load for both the `morphTo` and regular cases. | ||
function eagerLoadHelper(relation, relationName, handled, options) { | ||
return function(resp) { | ||
var relatedModels = relation.pushModels(relationName, handled, resp); | ||
var relatedData = handled.relatedData; | ||
// Handles the eager load for both the `morphTo` and regular cases. | ||
function eagerLoadHelper(relation, relationName, handled, options) { | ||
return function(resp) { | ||
var relatedModels = relation.pushModels(relationName, handled, resp); | ||
var relatedData = handled.relatedData; | ||
// If there is a response, fetch additional nested eager relations, if any. | ||
if (resp.length > 0 && options.withRelated) { | ||
var relatedModel = relatedData.createModel(); | ||
// If there is a response, fetch additional nested eager relations, if any. | ||
if (resp.length > 0 && options.withRelated) { | ||
var relatedModel = relatedData.createModel(); | ||
// If this is a `morphTo` relation, we need to do additional processing | ||
// to ensure we don't try to load any relations that don't look to exist. | ||
if (relatedData.type === 'morphTo') { | ||
var withRelated = filterRelated(relatedModel, options); | ||
if (withRelated.length === 0) return; | ||
options = _.extend({}, options, {withRelated: withRelated}); | ||
} | ||
return new EagerRelation(relatedModels, resp, relatedModel).fetch(options).yield(resp); | ||
// If this is a `morphTo` relation, we need to do additional processing | ||
// to ensure we don't try to load any relations that don't look to exist. | ||
if (relatedData.type === 'morphTo') { | ||
var withRelated = filterRelated(relatedModel, options); | ||
if (withRelated.length === 0) return; | ||
options = _.extend({}, options, {withRelated: withRelated}); | ||
} | ||
}; | ||
} | ||
return new EagerRelation(relatedModels, resp, relatedModel).fetch(options).yield(resp); | ||
} | ||
}; | ||
} | ||
// Filters the `withRelated` on a `morphTo` relation, to ensure that only valid | ||
// relations are attempted for loading. | ||
function filterRelated(relatedModel, options) { | ||
// Filters the `withRelated` on a `morphTo` relation, to ensure that only valid | ||
// relations are attempted for loading. | ||
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) { | ||
for (var key in val) { | ||
var seg = key.split('.')[0]; | ||
if (_.isFunction(relatedModel[seg])) memo.push(val); | ||
} | ||
return memo; | ||
}, []); | ||
} | ||
}); | ||
})( | ||
typeof define === 'function' && define.amd ? define : function (factory) { factory(require, exports); } | ||
); | ||
// 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) { | ||
for (var key in val) { | ||
var seg = key.split('.')[0]; | ||
if (_.isFunction(relatedModel[seg])) memo.push(val); | ||
} | ||
return memo; | ||
}, []); | ||
} |
// Helpers | ||
// --------------- | ||
(function(define) { | ||
"use strict"; | ||
var _ = require('lodash'); | ||
define(function(require, exports) { | ||
exports.Helpers = { | ||
var _ = require('lodash'); | ||
// Sets the constraints necessary during a `model.save` call. | ||
saveConstraints: function(model, relatedData) { | ||
var data = {}; | ||
if (relatedData && relatedData.type && relatedData.type !== 'belongsToMany') { | ||
data[relatedData.key('foreignKey')] = relatedData.parentFk; | ||
if (relatedData.isMorph()) data[relatedData.key('morphKey')] = relatedData.key('morphValue'); | ||
} | ||
return model.set(data); | ||
}, | ||
exports.Helpers = { | ||
// Finds the specific `morphTo` table we should be working with, or throws | ||
// an error if none is matched. | ||
morphCandidate: function(candidates, foreignTable) { | ||
var Target = _.find(candidates, function(Candidate) { | ||
return (_.result(Candidate.prototype, 'tableName') === foreignTable); | ||
}); | ||
if (!Target) { | ||
throw new Error('The target polymorphic model was not found'); | ||
} | ||
return Target; | ||
}, | ||
// Sets the constraints necessary during a `model.save` call. | ||
saveConstraints: function(model, relatedData) { | ||
var data = {}; | ||
if (relatedData && relatedData.type && relatedData.type !== 'belongsToMany') { | ||
data[relatedData.key('foreignKey')] = relatedData.parentFk; | ||
if (relatedData.isMorph()) data[relatedData.key('morphKey')] = relatedData.key('morphValue'); | ||
// If there are no arguments, return the current object's | ||
// query builder (or create and return a new one). If there are arguments, | ||
// call the query builder with the first argument, applying the rest. | ||
// If the first argument is an object, assume the keys are query builder | ||
// methods, and the values are the arguments for the query. | ||
query: function(obj, args) { | ||
obj._knex = obj._knex || obj._builder(_.result(obj, 'tableName')); | ||
if (args.length === 0) return obj._knex; | ||
var method = args[0]; | ||
if (_.isFunction(method)) { | ||
method.call(obj._knex, obj._knex); | ||
} else if (_.isObject(method)) { | ||
for (var key in method) { | ||
var target = _.isArray(method[key]) ? method[key] : [method[key]]; | ||
obj._knex[key].apply(obj._knex, target); | ||
} | ||
return model.set(data); | ||
}, | ||
// Finds the specific `morphTo` table we should be working with, or throws | ||
// an error if none is matched. | ||
morphCandidate: function(candidates, foreignTable) { | ||
var Target = _.find(candidates, function(Candidate) { | ||
return (_.result(Candidate.prototype, 'tableName') === foreignTable); | ||
}); | ||
if (!Target) { | ||
throw new Error('The target polymorphic model was not found'); | ||
} | ||
return Target; | ||
}, | ||
// If there are no arguments, return the current object's | ||
// query builder (or create and return a new one). If there are arguments, | ||
// call the query builder with the first argument, applying the rest. | ||
// If the first argument is an object, assume the keys are query builder | ||
// methods, and the values are the arguments for the query. | ||
query: function(obj, args) { | ||
obj._knex = obj._knex || obj._builder(_.result(obj, 'tableName')); | ||
if (args.length === 0) return obj._knex; | ||
var method = args[0]; | ||
if (_.isFunction(method)) { | ||
method.call(obj._knex, obj._knex); | ||
} else if (_.isObject(method)) { | ||
for (var key in method) { | ||
var target = _.isArray(method[key]) ? method[key] : [method[key]]; | ||
obj._knex[key].apply(obj._knex, target); | ||
} | ||
} else { | ||
obj._knex[method].apply(obj._knex, args.slice(1)); | ||
} | ||
return obj; | ||
} else { | ||
obj._knex[method].apply(obj._knex, args.slice(1)); | ||
} | ||
return obj; | ||
} | ||
}; | ||
}); | ||
})( | ||
typeof define === 'function' && define.amd ? define : function (factory) { factory(require, exports); } | ||
); | ||
}; |
// Model | ||
// --------------- | ||
(function(define) { | ||
var _ = require('lodash'); | ||
"use strict"; | ||
var Sync = require('./sync').Sync; | ||
var Helpers = require('./helpers').Helpers; | ||
var EagerRelation = require('./eager').EagerRelation; | ||
define(function(require, exports) { | ||
var ModelBase = require('../base/model').ModelBase; | ||
var Promise = require('../base/promise').Promise; | ||
var _ = require('lodash'); | ||
exports.Model = ModelBase.extend({ | ||
var Sync = require('./sync').Sync; | ||
var Helpers = require('./helpers').Helpers; | ||
var EagerRelation = require('./eager').EagerRelation; | ||
// 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 ModelBase = require('../base/model').ModelBase; | ||
var Promise = require('../base/promise').Promise; | ||
// 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); | ||
}, | ||
exports.Model = ModelBase.extend({ | ||
// 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 `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); | ||
}, | ||
// A `belongsToMany` relation is when there are many-to-many relation | ||
// between two models, with a joining table. | ||
belongsToMany: function(Target, joinTableName, foreignKey, otherKey) { | ||
return this._relation('belongsToMany', Target, { | ||
joinTableName: joinTableName, foreignKey: foreignKey, otherKey: otherKey | ||
}).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); | ||
}, | ||
// A `morphOne` relation is a one-to-one polymorphic association from this model | ||
// to another model. | ||
morphOne: function(Target, name, morphValue) { | ||
return this._morphOneOrMany(Target, name, morphValue, 'morphOne'); | ||
}, | ||
// 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); | ||
}, | ||
// A `morphMany` relation is a polymorphic many-to-one relation from this model | ||
// to many another models. | ||
morphMany: function(Target, name, morphValue) { | ||
return this._morphOneOrMany(Target, name, morphValue, 'morphMany'); | ||
}, | ||
// A `belongsToMany` relation is when there are many-to-many relation | ||
// between two models, with a joining table. | ||
belongsToMany: function(Target, joinTableName, foreignKey, otherKey) { | ||
return this._relation('belongsToMany', Target, { | ||
joinTableName: joinTableName, foreignKey: foreignKey, otherKey: otherKey | ||
}).init(this); | ||
}, | ||
// Defines the opposite end of a `morphOne` or `morphMany` relationship, where | ||
// the alternate end of the polymorphic model is defined. | ||
morphTo: function(morphName) { | ||
if (!_.isString(morphName)) throw new Error('The `morphTo` name must be specified.'); | ||
return this._relation('morphTo', null, {morphName: morphName, candidates: _.rest(arguments)}).init(this); | ||
}, | ||
// A `morphOne` relation is a one-to-one polymorphic association from this model | ||
// to another model. | ||
morphOne: function(Target, name, morphValue) { | ||
return this._morphOneOrMany(Target, name, morphValue, 'morphOne'); | ||
}, | ||
// 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}); | ||
}, | ||
// A `morphMany` relation is a polymorphic many-to-one relation from this model | ||
// to many another models. | ||
morphMany: function(Target, name, morphValue) { | ||
return this._morphOneOrMany(Target, name, morphValue, 'morphMany'); | ||
}, | ||
// 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) : {}; | ||
// Defines the opposite end of a `morphOne` or `morphMany` relationship, where | ||
// the alternate end of the polymorphic model is defined. | ||
morphTo: function(morphName) { | ||
if (!_.isString(morphName)) throw new Error('The `morphTo` name must be specified.'); | ||
return this._relation('morphTo', null, {morphName: morphName, candidates: _.rest(arguments)}).init(this); | ||
}, | ||
// Run the `first` call on the `sync` object to fetch a single model. | ||
var sync = this.sync(options) | ||
.first() | ||
.bind(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}); | ||
}, | ||
// 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 Error('EmptyResponse'); | ||
return Promise.reject(null); | ||
} | ||
}) | ||
// 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) : {}; | ||
// Now, load all of the data into the model as necessary. | ||
.tap(handleResponse); | ||
// Run the `first` call on the `sync` object to fetch a single model. | ||
var sync = this.sync(options) | ||
.first() | ||
.bind(this) | ||
// 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. | ||
if (options.withRelated) { | ||
sync = sync.tap(handleEager(_.omit(options, 'columns'))); | ||
} | ||
// 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 Error('EmptyResponse'); | ||
return Promise.reject(null); | ||
} | ||
}) | ||
return sync.tap(function(response) { | ||
return this.triggerThen('fetched', this, response, options); | ||
}) | ||
.yield(this) | ||
.caught(function(err) { | ||
if (err === null) return err; | ||
throw err; | ||
}); | ||
// 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. | ||
if (options.withRelated) { | ||
sync = sync.tap(this._handleEager(_.omit(options, 'columns'))); | ||
} | ||
return sync.tap(function(response) { | ||
return this.triggerThen('fetched', this, response, options); | ||
// Eager loads relationships onto an already populated `Model` instance. | ||
load: Promise.method(function(relations, options) { | ||
return Promise.bind(this) | ||
.then(function() { | ||
return [this.toJSON({shallow: true})]; | ||
}) | ||
.yield(this) | ||
.caught(function(err) { | ||
if (err === null) return err; | ||
throw err; | ||
}); | ||
.then(handleEager(_.extend({}, options, { | ||
shallow: true, | ||
withRelated: _.isArray(relations) ? relations : [relations] | ||
}))).yield(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; | ||
// Eager loads relationships onto an already populated `Model` instance. | ||
load: Promise.method(function(relations, options) { | ||
return Promise.bind(this) | ||
.then(function() { | ||
return [this.toJSON({shallow: true})]; | ||
}) | ||
.then(this._handleEager(_.extend({}, options, { | ||
shallow: true, | ||
withRelated: _.isArray(relations) ? relations : [relations] | ||
}))).yield(this); | ||
}), | ||
// Handle both `"key", value` and `{key: value}` -style arguments. | ||
if (key == null || typeof key === "object") { | ||
attrs = key || {}; | ||
options = val || {}; | ||
} else { | ||
(attrs = {})[key] = val; | ||
options = options ? _.clone(options) : {}; | ||
} | ||
// 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; | ||
return Promise.bind(this).then(function() { | ||
return this.isNew(options); | ||
}).then(function(isNew) { | ||
// Handle both `"key", value` and `{key: value}` -style arguments. | ||
if (key == null || typeof key === "object") { | ||
attrs = key || {}; | ||
options = val || {}; | ||
} else { | ||
(attrs = {})[key] = val; | ||
options = options ? _.clone(options) : {}; | ||
} | ||
// 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)); | ||
return Promise.bind(this).then(function() { | ||
return this.isNew(options); | ||
}).then(function(isNew) { | ||
// 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 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 the model is new, based on whether the model has an `idAttribute` or not. | ||
var method = options.method || (options.method = isNew ? 'insert' : 'update'); | ||
var vals = attrs; | ||
// 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'); | ||
if (defaults) { | ||
vals = _.extend({}, defaults, this.attributes, vals); | ||
} | ||
// 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'); | ||
if (defaults) { | ||
vals = _.extend({}, defaults, this.attributes, vals); | ||
} | ||
} | ||
// Set the attributes on the model. | ||
this.set(vals, {silent: true}); | ||
// Set the attributes on the model. | ||
this.set(vals, {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); | ||
} | ||
// If there are any save constraints, set them on the model. | ||
if (this.relatedData && this.relatedData.type !== 'morphTo') { | ||
Helpers.saveConstraints(this, this.relatedData); | ||
} | ||
// Gives access to the `query` object in the `options`, in case we need it | ||
// in any event handlers. | ||
var sync = this.sync(options); | ||
options.query = sync.query; | ||
// Gives access to the `query` object in the `options`, in case we need it | ||
// in any event handlers. | ||
var sync = this.sync(options); | ||
options.query = sync.query; | ||
return Promise.all([ | ||
this.triggerThen((method === 'insert' ? 'creating' : 'updating'), this, attrs, options), | ||
this.triggerThen('saving', this, attrs, options) | ||
]) | ||
.bind(this) | ||
.then(function() { | ||
return sync[options.method](method === 'update' && options.patch ? attrs : this.attributes); | ||
}) | ||
.then(function(resp) { | ||
return Promise.all([ | ||
this.triggerThen((method === 'insert' ? 'creating' : 'updating'), this, attrs, options), | ||
this.triggerThen('saving', this, attrs, options) | ||
]) | ||
.bind(this) | ||
.then(function() { | ||
return sync[options.method](method === 'update' && options.patch ? attrs : this.attributes); | ||
}) | ||
.then(function(resp) { | ||
// After a successful database save, the id is updated if the model was created | ||
if (method === 'insert' && resp) { | ||
this.attributes[this.idAttribute] = this[this.idAttribute] = resp[0]; | ||
} | ||
// After a successful database save, the id is updated if the model was created | ||
if (method === 'insert' && this.id == null) { | ||
this.attributes[this.idAttribute] = this.id = resp[0]; | ||
} else if (method === 'update' && resp === 0) { | ||
throw new Error('No rows were affected in the update, did you mean to pass the {insert: true} option?'); | ||
} | ||
// In case we need to reference the `previousAttributes` for the this | ||
// in the following event handlers. | ||
options.previousAttributes = this._previousAttributes; | ||
// In case we need to reference the `previousAttributes` for the this | ||
// in the following event handlers. | ||
options.previousAttributes = this._previousAttributes; | ||
this._reset(); | ||
this._reset(); | ||
return Promise.all([ | ||
this.triggerThen((method === 'insert' ? 'created' : 'updated'), this, resp, options), | ||
this.triggerThen('saved', this, resp, options) | ||
]); | ||
return Promise.all([ | ||
this.triggerThen((method === 'insert' ? 'created' : 'updated'), this, resp, options), | ||
this.triggerThen('saved', this, resp, options) | ||
]); | ||
}); | ||
}); | ||
}).yield(this); | ||
}), | ||
}).yield(this); | ||
}), | ||
// Reset the query builder, called internally | ||
// each time a query is run. | ||
resetQuery: function() { | ||
this._knex = null; | ||
return this; | ||
}, | ||
// Reset the query builder, called internally | ||
// each time a query is run. | ||
resetQuery: function() { | ||
this._knex = null; | ||
return this; | ||
}, | ||
// Returns an instance of the query builder. | ||
query: function() { | ||
return Helpers.query(this, _.toArray(arguments)); | ||
}, | ||
// Returns an instance of the query builder. | ||
query: function() { | ||
return Helpers.query(this, _.toArray(arguments)); | ||
}, | ||
// Creates and returns a new `Sync` instance. | ||
sync: function(options) { | ||
return new Sync(this, options); | ||
}, | ||
// Creates and returns a new `Sync` instance. | ||
sync: function(options) { | ||
return new Sync(this, options); | ||
}, | ||
// Helper for setting up the `morphOne` or `morphMany` relations. | ||
_morphOneOrMany: function(Target, morphName, morphValue, type) { | ||
if (!morphName || !Target) throw new Error('The polymorphic `name` and `Target` are required.'); | ||
return this._relation(type, Target, {morphName: morphName, morphValue: morphValue}).init(this); | ||
}, | ||
// Helper for setting up the `morphOne` or `morphMany` relations. | ||
_morphOneOrMany: function(Target, morphName, morphValue, type) { | ||
if (!morphName || !Target) throw new Error('The polymorphic `name` and `Target` are required.'); | ||
return this._relation(type, Target, {morphName: morphName, morphValue: morphValue}).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) { | ||
var relatedData = this.relatedData; | ||
this.set(this.parse(response[0]), {silent: true})._reset(); | ||
if (relatedData && relatedData.isJoined()) { | ||
relatedData.parsePivot([this]); | ||
} | ||
}, | ||
}); | ||
// Handle the related data loading on the model. | ||
_handleEager: function(options) { | ||
return function(response) { | ||
return new EagerRelation([this], response, this).fetch(options); | ||
}; | ||
} | ||
// 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 | ||
function handleResponse(response) { | ||
var relatedData = this.relatedData; | ||
this.set(this.parse(response[0]), {silent: true})._reset(); | ||
if (relatedData && relatedData.isJoined()) { | ||
relatedData.parsePivot([this]); | ||
} | ||
} | ||
}); | ||
}); | ||
})( | ||
typeof define === 'function' && define.amd ? define : function (factory) { factory(require, exports); } | ||
); | ||
// Handle the related data loading on the model. | ||
function handleEager(options) { | ||
return function(response) { | ||
return new EagerRelation([this], response, this).fetch(options); | ||
}; | ||
} |
// Relation | ||
// --------------- | ||
(function(define) { | ||
var _ = require('lodash'); | ||
var inflection = require('inflection'); | ||
"use strict"; | ||
var Helpers = require('./helpers').Helpers; | ||
define(function(require, exports) { | ||
var ModelBase = require('../base/model').ModelBase; | ||
var RelationBase = require('../base/relation').RelationBase; | ||
var Promise = require('../base/promise').Promise; | ||
var _ = require('lodash'); | ||
var inflection = require('inflection'); | ||
var push = [].push; | ||
var Helpers = require('./helpers').Helpers; | ||
exports.Relation = RelationBase.extend({ | ||
var ModelBase = require('../base/model').ModelBase; | ||
var RelationBase = require('../base/relation').RelationBase; | ||
var Promise = require('../base/promise').Promise; | ||
// Assembles the new model or collection we're creating an instance of, | ||
// gathering any relevant primitives from the parent object, | ||
// without keeping any hard references. | ||
init: function(parent) { | ||
this.parentId = parent.id; | ||
this.parentTableName = _.result(parent, 'tableName'); | ||
this.parentIdAttribute = _.result(parent, 'idAttribute'); | ||
var push = [].push; | ||
exports.Relation = RelationBase.extend({ | ||
// Assembles the new model or collection we're creating an instance of, | ||
// gathering any relevant primitives from the parent object, | ||
// without keeping any hard references. | ||
init: function(parent) { | ||
this.parentId = parent.id; | ||
this.parentTableName = _.result(parent, 'tableName'); | ||
this.parentIdAttribute = _.result(parent, 'idAttribute'); | ||
if (this.isInverse()) { | ||
// If the parent object is eager loading, and it's a polymorphic `morphTo` relation, | ||
// we can't know what the target will be until the models are sorted and matched. | ||
if (this.type === 'morphTo' && !parent._isEager) { | ||
this.target = Helpers.morphCandidate(this.candidates, parent.get(this.key('morphKey'))); | ||
this.targetTableName = _.result(this.target.prototype, 'tableName'); | ||
this.targetIdAttribute = _.result(this.target.prototype, 'idAttribute'); | ||
} | ||
this.parentFk = parent.get(this.key('foreignKey')); | ||
} else { | ||
this.parentFk = parent.id; | ||
if (this.isInverse()) { | ||
// If the parent object is eager loading, and it's a polymorphic `morphTo` relation, | ||
// we can't know what the target will be until the models are sorted and matched. | ||
if (this.type === 'morphTo' && !parent._isEager) { | ||
this.target = Helpers.morphCandidate(this.candidates, parent.get(this.key('morphKey'))); | ||
this.targetTableName = _.result(this.target.prototype, 'tableName'); | ||
this.targetIdAttribute = _.result(this.target.prototype, 'idAttribute'); | ||
} | ||
this.parentFk = parent.get(this.key('foreignKey')); | ||
} else { | ||
this.parentFk = parent.id; | ||
} | ||
var target = this.target ? this.relatedInstance() : {}; | ||
target.relatedData = this; | ||
var target = this.target ? this.relatedInstance() : {}; | ||
target.relatedData = this; | ||
if (this.type === 'belongsToMany') { | ||
_.extend(target, pivotHelpers); | ||
} | ||
if (this.type === 'belongsToMany') { | ||
_.extend(target, pivotHelpers); | ||
} | ||
return target; | ||
}, | ||
return target; | ||
}, | ||
// Initializes a `through` relation, setting the `Target` model and `options`, | ||
// which includes any additional keys for the relation. | ||
through: function(source, Target, options) { | ||
var type = this.type; | ||
if (type !== 'hasOne' && type !== 'hasMany' && type !== 'belongsToMany' && type !== 'belongsTo') { | ||
throw new Error('`through` is only chainable from `hasOne`, `belongsTo`, `hasMany`, or `belongsToMany`'); | ||
} | ||
// Initializes a `through` relation, setting the `Target` model and `options`, | ||
// which includes any additional keys for the relation. | ||
through: function(source, Target, options) { | ||
var type = this.type; | ||
if (type !== 'hasOne' && type !== 'hasMany' && type !== 'belongsToMany' && type !== 'belongsTo') { | ||
throw new Error('`through` is only chainable from `hasOne`, `belongsTo`, `hasMany`, or `belongsToMany`'); | ||
} | ||
this.throughTarget = Target; | ||
this.throughTableName = _.result(Target.prototype, 'tableName'); | ||
this.throughIdAttribute = _.result(Target.prototype, 'idAttribute'); | ||
this.throughTarget = Target; | ||
this.throughTableName = _.result(Target.prototype, 'tableName'); | ||
this.throughIdAttribute = _.result(Target.prototype, 'idAttribute'); | ||
// Set the parentFk as appropriate now. | ||
if (this.type === 'belongsTo') { | ||
this.parentFk = this.parentId; | ||
} | ||
// Set the parentFk as appropriate now. | ||
if (this.type === 'belongsTo') { | ||
this.parentFk = this.parentId; | ||
} | ||
_.extend(this, options); | ||
_.extend(source, pivotHelpers); | ||
_.extend(this, options); | ||
_.extend(source, pivotHelpers); | ||
// Set the appropriate foreign key if we're doing a belongsToMany, for convenience. | ||
if (this.type === 'belongsToMany') { | ||
this.foreignKey = this.throughForeignKey; | ||
} | ||
// Set the appropriate foreign key if we're doing a belongsToMany, for convenience. | ||
if (this.type === 'belongsToMany') { | ||
this.foreignKey = this.throughForeignKey; | ||
} | ||
return source; | ||
}, | ||
return source; | ||
}, | ||
// Generates and returns a specified key, for convenience... one of | ||
// `foreignKey`, `otherKey`, `throughForeignKey`. | ||
key: function(keyName) { | ||
if (this[keyName]) return this[keyName]; | ||
if (keyName === 'otherKey') { | ||
return this[keyName] = singularMemo(this.targetTableName) + '_' + this.targetIdAttribute; | ||
} | ||
if (keyName === 'throughForeignKey') { | ||
return this[keyName] = singularMemo(this.joinTable()) + '_' + this.throughIdAttribute; | ||
} | ||
if (keyName === 'foreignKey') { | ||
if (this.type === 'morphTo') return this[keyName] = this.morphName + '_id'; | ||
if (this.type === 'belongsTo') return this[keyName] = singularMemo(this.targetTableName) + '_' + this.targetIdAttribute; | ||
if (this.isMorph()) return this[keyName] = this.morphName + '_id'; | ||
return this[keyName] = singularMemo(this.parentTableName) + '_' + this.parentIdAttribute; | ||
} | ||
if (keyName === 'morphKey') return this[keyName] = this.morphName + '_type'; | ||
if (keyName === 'morphValue') return this[keyName] = this.parentTableName || this.targetTableName; | ||
}, | ||
// Generates and returns a specified key, for convenience... one of | ||
// `foreignKey`, `otherKey`, `throughForeignKey`. | ||
key: function(keyName) { | ||
if (this[keyName]) return this[keyName]; | ||
if (keyName === 'otherKey') { | ||
return this[keyName] = singularMemo(this.targetTableName) + '_' + this.targetIdAttribute; | ||
} | ||
if (keyName === 'throughForeignKey') { | ||
return this[keyName] = singularMemo(this.joinTable()) + '_' + this.throughIdAttribute; | ||
} | ||
if (keyName === 'foreignKey') { | ||
if (this.type === 'morphTo') return this[keyName] = this.morphName + '_id'; | ||
if (this.type === 'belongsTo') return this[keyName] = singularMemo(this.targetTableName) + '_' + this.targetIdAttribute; | ||
if (this.isMorph()) return this[keyName] = this.morphName + '_id'; | ||
return this[keyName] = singularMemo(this.parentTableName) + '_' + this.parentIdAttribute; | ||
} | ||
if (keyName === 'morphKey') return this[keyName] = this.morphName + '_type'; | ||
if (keyName === 'morphValue') return this[keyName] = this.parentTableName || this.targetTableName; | ||
}, | ||
// Injects the necessary `select` constraints into a `knex` query builder. | ||
selectConstraints: function(knex, options) { | ||
var resp = options.parentResponse; | ||
// Injects the necessary `select` constraints into a `knex` query builder. | ||
selectConstraints: function(knex, options) { | ||
var resp = options.parentResponse; | ||
// The base select column | ||
if (knex.columns.length === 0 && (!options.columns || options.columns.length === 0)) { | ||
knex.columns.push(this.targetTableName + '.*'); | ||
} else if (_.isArray(options.columns) && options.columns.length > 0) { | ||
push.apply(knex.columns, options.columns); | ||
} | ||
// The base select column | ||
if (knex.columns.length === 0 && (!options.columns || options.columns.length === 0)) { | ||
knex.columns.push(this.targetTableName + '.*'); | ||
} else if (_.isArray(options.columns) && options.columns.length > 0) { | ||
push.apply(knex.columns, options.columns); | ||
} | ||
// The `belongsToMany` and `through` relations have joins & pivot columns. | ||
if (this.isJoined()) { | ||
this.joinClauses(knex); | ||
this.joinColumns(knex); | ||
} | ||
// The `belongsToMany` and `through` relations have joins & pivot columns. | ||
if (this.isJoined()) { | ||
this.joinClauses(knex); | ||
this.joinColumns(knex); | ||
} | ||
// If this is a single relation and we're not eager loading, | ||
// limit the query to a single item. | ||
if (this.isSingle() && !resp) knex.limit(1); | ||
// If this is a single relation and we're not eager loading, | ||
// limit the query to a single item. | ||
if (this.isSingle() && !resp) knex.limit(1); | ||
// Finally, add (and validate) the where conditions, necessary for constraining the relation. | ||
this.whereClauses(knex, resp); | ||
}, | ||
// Finally, add (and validate) the where conditions, necessary for constraining the relation. | ||
this.whereClauses(knex, resp); | ||
}, | ||
// Inject & validates necessary `through` constraints for the current model. | ||
joinColumns: function(knex) { | ||
var columns = []; | ||
var joinTable = this.joinTable(); | ||
if (this.isThrough()) columns.push(this.throughIdAttribute); | ||
columns.push(this.key('foreignKey')); | ||
if (this.type === 'belongsToMany') columns.push(this.key('otherKey')); | ||
push.apply(columns, this.pivotColumns); | ||
push.apply(knex.columns, _.map(columns, function(col) { | ||
return joinTable + '.' + col + ' as _pivot_' + col; | ||
})); | ||
}, | ||
// Inject & validates necessary `through` constraints for the current model. | ||
joinColumns: function(knex) { | ||
var columns = []; | ||
var joinTable = this.joinTable(); | ||
if (this.isThrough()) columns.push(this.throughIdAttribute); | ||
columns.push(this.key('foreignKey')); | ||
if (this.type === 'belongsToMany') columns.push(this.key('otherKey')); | ||
push.apply(columns, this.pivotColumns); | ||
push.apply(knex.columns, _.map(columns, function(col) { | ||
return joinTable + '.' + col + ' as _pivot_' + col; | ||
})); | ||
}, | ||
// Generates the join clauses necessary for the current relation. | ||
joinClauses: function(knex) { | ||
var joinTable = this.joinTable(); | ||
// Generates the join clauses necessary for the current relation. | ||
joinClauses: function(knex) { | ||
var joinTable = this.joinTable(); | ||
if (this.type === 'belongsTo' || this.type === 'belongsToMany') { | ||
if (this.type === 'belongsTo' || this.type === 'belongsToMany') { | ||
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') | ||
); | ||
} | ||
} else { | ||
// A `belongsTo` -> `through` is currently the only relation with two joins. | ||
if (this.type === 'belongsTo') { | ||
knex.join( | ||
joinTable, | ||
this.parentTableName, | ||
joinTable + '.' + this.throughIdAttribute, '=', | ||
this.targetTableName + '.' + this.key('throughForeignKey') | ||
this.parentTableName + '.' + this.key('throughForeignKey') | ||
); | ||
} | ||
}, | ||
// Check that there isn't an incorrect foreign key set, vs. the one | ||
// passed in when the relation was formed. | ||
whereClauses: function(knex, resp) { | ||
var key; | ||
} else { | ||
knex.join( | ||
joinTable, | ||
joinTable + '.' + this.throughIdAttribute, '=', | ||
this.targetTableName + '.' + this.key('throughForeignKey') | ||
); | ||
} | ||
}, | ||
if (this.isJoined()) { | ||
var targetTable = this.type === 'belongsTo' ? this.parentTableName : this.joinTable(); | ||
key = targetTable + '.' + (this.type === 'belongsTo' ? this.parentIdAttribute : this.key('foreignKey')); | ||
} else { | ||
key = this.targetTableName + '.' + | ||
(this.isInverse() ? this.targetIdAttribute : this.key('foreignKey')); | ||
} | ||
// Check that there isn't an incorrect foreign key set, vs. the one | ||
// passed in when the relation was formed. | ||
whereClauses: function(knex, resp) { | ||
var key; | ||
knex[resp ? 'whereIn' : 'where'](key, resp ? this.eagerKeys(resp) : this.parentFk); | ||
if (this.isJoined()) { | ||
var targetTable = this.type === 'belongsTo' ? this.parentTableName : this.joinTable(); | ||
key = targetTable + '.' + (this.type === 'belongsTo' ? this.parentIdAttribute : this.key('foreignKey')); | ||
} else { | ||
key = this.targetTableName + '.' + | ||
(this.isInverse() ? this.targetIdAttribute : this.key('foreignKey')); | ||
} | ||
if (this.isMorph()) { | ||
knex.where(this.targetTableName + '.' + this.key('morphKey'), this.key('morphValue')); | ||
} | ||
}, | ||
knex[resp ? 'whereIn' : 'where'](key, resp ? this.eagerKeys(resp) : this.parentFk); | ||
// Fetches all `eagerKeys` from the current relation. | ||
eagerKeys: function(resp) { | ||
return _.uniq(_.pluck(resp, this.isInverse() ? this.key('foreignKey') : this.parentIdAttribute)); | ||
}, | ||
if (this.isMorph()) { | ||
knex.where(this.targetTableName + '.' + this.key('morphKey'), this.key('morphValue')); | ||
} | ||
}, | ||
// Generates the appropriate standard join table. | ||
joinTable: function() { | ||
if (this.isThrough()) return this.throughTableName; | ||
return this.joinTableName || [ | ||
this.parentTableName, | ||
this.targetTableName | ||
].sort().join('_'); | ||
}, | ||
// Fetches all `eagerKeys` from the current relation. | ||
eagerKeys: function(resp) { | ||
return _.uniq(_.pluck(resp, this.isInverse() ? this.key('foreignKey') : this.parentIdAttribute)); | ||
}, | ||
// Creates a new model or collection instance, depending on | ||
// the `relatedData` settings and the models passed in. | ||
relatedInstance: function(models) { | ||
models || (models = []); | ||
// Generates the appropriate standard join table. | ||
joinTable: function() { | ||
if (this.isThrough()) return this.throughTableName; | ||
return this.joinTableName || [ | ||
this.parentTableName, | ||
this.targetTableName | ||
].sort().join('_'); | ||
}, | ||
var Target = this.target; | ||
// Creates a new model or collection instance, depending on | ||
// the `relatedData` settings and the models passed in. | ||
relatedInstance: function(models) { | ||
models || (models = []); | ||
// If it's a single model, check whether there's already a model | ||
// we can pick from... otherwise create a new instance. | ||
if (this.isSingle()) { | ||
if (!(Target.prototype instanceof ModelBase)) { | ||
throw new Error('The `'+this.type+'` related object must be a Bookshelf.Model'); | ||
} | ||
return models[0] || new Target(); | ||
} | ||
var Target = this.target; | ||
// Allows us to just use a model, but create a temporary | ||
// collection for a "*-many" relation. | ||
if (Target.prototype instanceof ModelBase) { | ||
Target = this.Collection.extend({ | ||
model: Target, | ||
_builder: Target.prototype._builder | ||
}); | ||
// If it's a single model, check whether there's already a model | ||
// we can pick from... otherwise create a new instance. | ||
if (this.isSingle()) { | ||
if (!(Target.prototype instanceof ModelBase)) { | ||
throw new Error('The `'+this.type+'` related object must be a Bookshelf.Model'); | ||
} | ||
return new Target(models, {parse: true}); | ||
}, | ||
return models[0] || new Target(); | ||
} | ||
// Groups the related response according to the type of relationship | ||
// we're handling, for easy attachment to the parent models. | ||
eagerPair: function(relationName, related, parentModels) { | ||
var model; | ||
// Allows us to just use a model, but create a temporary | ||
// collection for a "*-many" relation. | ||
if (Target.prototype instanceof ModelBase) { | ||
Target = this.Collection.extend({ | ||
model: Target, | ||
_builder: Target.prototype._builder | ||
}); | ||
} | ||
return new Target(models, {parse: true}); | ||
}, | ||
// If this is a morphTo, we only want to pair on the morphValue for the current relation. | ||
if (this.type === 'morphTo') { | ||
parentModels = _.filter(parentModels, function(model) { | ||
return model.get(this.key('morphKey')) === this.key('morphValue'); | ||
}, this); | ||
} | ||
// Groups the related response according to the type of relationship | ||
// we're handling, for easy attachment to the parent models. | ||
eagerPair: function(relationName, related, parentModels) { | ||
var model; | ||
// If this is a `through` or `belongsToMany` relation, we need to cleanup & setup the `interim` model. | ||
if (this.isJoined()) related = this.parsePivot(related); | ||
// Group all of the related models for easier association with their parent models. | ||
var grouped = _.groupBy(related, function(model) { | ||
return model.pivot ? model.pivot.get(this.key('foreignKey')) : | ||
this.isInverse() ? model.id : model.get(this.key('foreignKey')); | ||
// If this is a morphTo, we only want to pair on the morphValue for the current relation. | ||
if (this.type === 'morphTo') { | ||
parentModels = _.filter(parentModels, function(model) { | ||
return model.get(this.key('morphKey')) === this.key('morphValue'); | ||
}, 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++) { | ||
model = parentModels[i]; | ||
var groupedKey = this.isInverse() ? model.get(this.key('foreignKey')) : model.id; | ||
var relation = model.relations[relationName] = this.relatedInstance(grouped[groupedKey]); | ||
relation.relatedData = this; | ||
} | ||
// If this is a `through` or `belongsToMany` relation, we need to cleanup & setup the `interim` model. | ||
if (this.isJoined()) related = this.parsePivot(related); | ||
// Now that related models have been successfully paired, update each with | ||
// its parsed attributes | ||
for (i = 0, l = related.length; i < l; i++) { | ||
model = related[i]; | ||
model.attributes = model.parse(model.attributes); | ||
} | ||
// Group all of the related models for easier association with their parent models. | ||
var grouped = _.groupBy(related, function(model) { | ||
return model.pivot ? model.pivot.get(this.key('foreignKey')) : | ||
this.isInverse() ? model.id : model.get(this.key('foreignKey')); | ||
}, this); | ||
return related; | ||
}, | ||
// 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++) { | ||
model = parentModels[i]; | ||
var groupedKey = this.isInverse() ? model.get(this.key('foreignKey')) : model.id; | ||
var relation = model.relations[relationName] = this.relatedInstance(grouped[groupedKey]); | ||
relation.relatedData = this; | ||
if (this.isJoined()) _.extend(relation, pivotHelpers); | ||
} | ||
// The `models` is an array of models returned from the fetch, | ||
// after they're `set`... parsing out any of the `_pivot_` items from the | ||
// join table and assigning them on the pivot model or object as appropriate. | ||
parsePivot: function(models) { | ||
var Through = this.throughTarget; | ||
return _.map(models, function(model) { | ||
var data = {}, keep = {}, attrs = model.attributes, through; | ||
if (Through) through = new Through(); | ||
for (var key in attrs) { | ||
if (key.indexOf('_pivot_') === 0) { | ||
data[key.slice(7)] = attrs[key]; | ||
} else { | ||
keep[key] = attrs[key]; | ||
} | ||
// Now that related models have been successfully paired, update each with | ||
// its parsed attributes | ||
for (i = 0, l = related.length; i < l; i++) { | ||
model = related[i]; | ||
model.attributes = model.parse(model.attributes); | ||
} | ||
return related; | ||
}, | ||
// The `models` is an array of models returned from the fetch, | ||
// after they're `set`... parsing out any of the `_pivot_` items from the | ||
// join table and assigning them on the pivot model or object as appropriate. | ||
parsePivot: function(models) { | ||
var Through = this.throughTarget; | ||
return _.map(models, function(model) { | ||
var data = {}, keep = {}, attrs = model.attributes, through; | ||
if (Through) through = new Through(); | ||
for (var key in attrs) { | ||
if (key.indexOf('_pivot_') === 0) { | ||
data[key.slice(7)] = attrs[key]; | ||
} else { | ||
keep[key] = attrs[key]; | ||
} | ||
model.attributes = keep; | ||
if (!_.isEmpty(data)) { | ||
model.pivot = through ? through.set(data, {silent: true}) : new this.Model(data, { | ||
tableName: this.joinTable() | ||
}); | ||
} | ||
return model; | ||
}, this); | ||
}, | ||
} | ||
model.attributes = keep; | ||
if (!_.isEmpty(data)) { | ||
model.pivot = through ? through.set(data, {silent: true}) : new this.Model(data, { | ||
tableName: this.joinTable() | ||
}); | ||
} | ||
return model; | ||
}, this); | ||
}, | ||
// A few predicates to help clarify some of the logic above. | ||
isThrough: function() { | ||
return (this.throughTarget != null); | ||
}, | ||
isJoined: function() { | ||
return (this.type === 'belongsToMany' || this.isThrough()); | ||
}, | ||
isMorph: function() { | ||
return (this.type === 'morphOne' || this.type === 'morphMany'); | ||
}, | ||
isSingle: function() { | ||
var type = this.type; | ||
return (type === 'hasOne' || type === 'belongsTo' || type === 'morphOne' || type === 'morphTo'); | ||
}, | ||
isInverse: function() { | ||
return (this.type === 'belongsTo' || this.type === 'morphTo'); | ||
}, | ||
// A few predicates to help clarify some of the logic above. | ||
isThrough: function() { | ||
return (this.throughTarget != null); | ||
}, | ||
isJoined: function() { | ||
return (this.type === 'belongsToMany' || this.isThrough()); | ||
}, | ||
isMorph: function() { | ||
return (this.type === 'morphOne' || this.type === 'morphMany'); | ||
}, | ||
isSingle: function() { | ||
var type = this.type; | ||
return (type === 'hasOne' || type === 'belongsTo' || type === 'morphOne' || type === 'morphTo'); | ||
}, | ||
isInverse: function() { | ||
return (this.type === 'belongsTo' || this.type === 'morphTo'); | ||
}, | ||
// Sets the `pivotColumns` to be retrieved along with the current model. | ||
withPivot: function(columns) { | ||
if (!_.isArray(columns)) columns = [columns]; | ||
this.pivotColumns || (this.pivotColumns = []); | ||
push.apply(this.pivotColumns, columns); | ||
// Sets the `pivotColumns` to be retrieved along with the current model. | ||
withPivot: function(columns) { | ||
if (!_.isArray(columns)) columns = [columns]; | ||
this.pivotColumns || (this.pivotColumns = []); | ||
push.apply(this.pivotColumns, columns); | ||
} | ||
}); | ||
// Simple memoization of the singularize call. | ||
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); | ||
} | ||
}; | ||
}()); | ||
}); | ||
// Specific to many-to-many relationships, these methods are mixed | ||
// into the `belongsToMany` relationships when they are created, | ||
// providing helpers for attaching and detaching related models. | ||
var pivotHelpers = { | ||
// Simple memoization of the singularize call. | ||
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); | ||
} | ||
}; | ||
}()); | ||
// Attach one or more "ids" from a foreign | ||
// table to the current. Creates & saves a new model | ||
// and attaches the model with a join table entry. | ||
attach: function(ids, options) { | ||
return this._handler('insert', ids, options); | ||
}, | ||
// Specific to many-to-many relationships, these methods are mixed | ||
// into the `belongsToMany` relationships when they are created, | ||
// providing helpers for attaching and detaching related models. | ||
var pivotHelpers = { | ||
// Detach related object from their pivot tables. | ||
// If a model or id is passed, it attempts to remove the | ||
// pivot table based on that foreign key. If a hash is passed, | ||
// it attempts to remove the item based on a where clause with | ||
// these parameters. If no parameters are specified, we assume we will | ||
// detach all related associations. | ||
detach: function(ids, options) { | ||
return this._handler('delete', ids, options); | ||
}, | ||
// Attach one or more "ids" from a foreign | ||
// table to the current. Creates & saves a new model | ||
// and attaches the model with a join table entry. | ||
attach: function(ids, options) { | ||
return this._handler('insert', ids, options); | ||
}, | ||
// Selects any additional columns on the pivot table, | ||
// taking a hash of columns which specifies the pivot | ||
// column name, and the value the column should take on the | ||
// output to the model attributes. | ||
withPivot: function(columns) { | ||
this.relatedData.withPivot(columns); | ||
return this; | ||
}, | ||
// Detach related object from their pivot tables. | ||
// If a model or id is passed, it attempts to remove the | ||
// pivot table based on that foreign key. If a hash is passed, | ||
// it attempts to remove the item based on a where clause with | ||
// these parameters. If no parameters are specified, we assume we will | ||
// detach all related associations. | ||
detach: function(ids, options) { | ||
return this._handler('delete', ids, options); | ||
}, | ||
// Helper for handling either the `attach` or `detach` call on | ||
// the `belongsToMany` or `hasOne` / `hasMany` :through relationship. | ||
_handler: Promise.method(function(method, ids, options) { | ||
var pending = []; | ||
if (ids == void 0) { | ||
if (method === 'insert') return Promise.resolve(this); | ||
if (method === 'delete') pending.push(this._processPivot(method, null, options)); | ||
} | ||
if (!_.isArray(ids)) ids = ids ? [ids] : []; | ||
for (var i = 0, l = ids.length; i < l; i++) { | ||
pending.push(this._processPivot(method, ids[i], options)); | ||
} | ||
return Promise.all(pending).yield(this); | ||
}), | ||
// Selects any additional columns on the pivot table, | ||
// taking a hash of columns which specifies the pivot | ||
// column name, and the value the column should take on the | ||
// output to the model attributes. | ||
withPivot: function(columns) { | ||
this.relatedData.withPivot(columns); | ||
return this; | ||
}, | ||
// Handles setting the appropriate constraints and shelling out | ||
// to either the `insert` or `delete` call for the current model, | ||
// returning a promise. | ||
_processPivot: Promise.method(function(method, item, options) { | ||
var data = {}; | ||
var relatedData = this.relatedData; | ||
data[relatedData.key('foreignKey')] = relatedData.parentFk; | ||
// Helper for handling either the `attach` or `detach` call on | ||
// the `belongsToMany` or `hasOne` / `hasMany` :through relationship. | ||
_handler: Promise.method(function(method, ids, options) { | ||
var pending = []; | ||
if (ids == void 0) { | ||
if (method === 'insert') return Promise.resolve(this); | ||
if (method === 'delete') pending.push(this._processPivot(method, null, options)); | ||
// If the item is an object, it's either a model | ||
// that we're looking to attach to this model, or | ||
// a hash of attributes to set in the relation. | ||
if (_.isObject(item)) { | ||
if (item instanceof ModelBase) { | ||
data[relatedData.key('otherKey')] = item.id; | ||
} else { | ||
_.extend(data, item); | ||
} | ||
if (!_.isArray(ids)) ids = ids ? [ids] : []; | ||
for (var i = 0, l = ids.length; i < l; i++) { | ||
pending.push(this._processPivot(method, ids[i], options)); | ||
} | ||
return Promise.all(pending).yield(this); | ||
}), | ||
// Handles setting the appropriate constraints and shelling out | ||
// to either the `insert` or `delete` call for the current model, | ||
// returning a promise. | ||
_processPivot: Promise.method(function(method, item, options) { | ||
var data = {}; | ||
var relatedData = this.relatedData; | ||
data[relatedData.key('foreignKey')] = relatedData.parentFk; | ||
// If the item is an object, it's either a model | ||
// that we're looking to attach to this model, or | ||
// a hash of attributes to set in the relation. | ||
if (_.isObject(item)) { | ||
if (item instanceof ModelBase) { | ||
data[relatedData.key('otherKey')] = item.id; | ||
} else { | ||
_.extend(data, item); | ||
} else if (item) { | ||
data[relatedData.key('otherKey')] = item; | ||
} | ||
var builder = this._builder(relatedData.joinTable()); | ||
if (options) { | ||
if (options.transacting) builder.transacting(options.transacting); | ||
if (options.debug) builder.debug(); | ||
} | ||
var collection = this; | ||
if (method === 'delete') { | ||
return builder.where(data).del().then(function() { | ||
var model; | ||
if (!item) return collection.reset(); | ||
if (model = collection.get(data[relatedData.key('otherKey')])) { | ||
collection.remove(model); | ||
} | ||
} else if (item) { | ||
data[relatedData.key('otherKey')] = item; | ||
} | ||
var builder = this._builder(relatedData.joinTable()); | ||
if (options && options.transacting) { | ||
builder.transacting(options.transacting); | ||
} | ||
var collection = this; | ||
if (method === 'delete') { | ||
return builder.where(data).del().then(function() { | ||
var model; | ||
if (!item) return collection.reset(); | ||
if (model = collection.get(data[relatedData.key('otherKey')])) { | ||
collection.remove(model); | ||
} | ||
}); | ||
} | ||
return builder.insert(data).then(function() { | ||
collection.add(item); | ||
}); | ||
}) | ||
} | ||
return builder.insert(data).then(function() { | ||
collection.add(item); | ||
}); | ||
}) | ||
}; | ||
}); | ||
})( | ||
typeof define === 'function' && define.amd ? define : function (factory) { factory(require, exports); } | ||
); | ||
}; |
// Sync | ||
// --------------- | ||
(function(define) { | ||
var _ = require('lodash'); | ||
var Promise = require('../base/promise').Promise; | ||
"use strict"; | ||
// Sync is the dispatcher for any database queries, | ||
// taking the "syncing" `model` or `collection` being queried, along with | ||
// a hash of options that are used in the various query methods. | ||
// If the `transacting` option is set, the query is assumed to be | ||
// part of a transaction, and this information is passed along to `Knex`. | ||
var Sync = function(syncing, options) { | ||
options || (options = {}); | ||
this.query = syncing.query(); | ||
this.syncing = syncing.resetQuery(); | ||
this.options = options; | ||
if (options.debug) this.query.debug(); | ||
if (options.transacting) this.query.transacting(options.transacting); | ||
}; | ||
define(function(require, exports) { | ||
_.extend(Sync.prototype, { | ||
var _ = require('lodash'); | ||
var Promise = require('../base/promise').Promise; | ||
// Select the first item from the database - only used by models. | ||
first: Promise.method(function() { | ||
this.query.where(this.syncing.format( | ||
_.extend(Object.create(null), this.syncing.attributes)) | ||
).limit(1); | ||
return this.select(); | ||
}), | ||
// Sync is the dispatcher for any database queries, | ||
// taking the "syncing" `model` or `collection` being queried, along with | ||
// a hash of options that are used in the various query methods. | ||
// If the `transacting` option is set, the query is assumed to be | ||
// part of a transaction, and this information is passed along to `Knex`. | ||
var Sync = function(syncing, options) { | ||
options || (options = {}); | ||
this.query = syncing.query(); | ||
this.syncing = syncing.resetQuery(); | ||
this.options = options; | ||
if (options.transacting) this.query.transacting(options.transacting); | ||
}; | ||
// Runs a `select` query on the database, adding any necessary relational | ||
// constraints, resetting the query when complete. If there are results and | ||
// eager loaded relations, those are fetched and returned on the model before | ||
// the promise is resolved. Any `success` handler passed in the | ||
// options will be called - used by both models & collections. | ||
select: Promise.method(function() { | ||
var columns, sync = this, | ||
options = this.options, relatedData = this.syncing.relatedData; | ||
_.extend(Sync.prototype, { | ||
// Inject all appropriate select costraints dealing with the relation | ||
// into the `knex` query builder for the current instance. | ||
if (relatedData) { | ||
relatedData.selectConstraints(this.query, options); | ||
} else { | ||
columns = options.columns; | ||
if (!_.isArray(columns)) columns = columns ? [columns] : [_.result(this.syncing, 'tableName') + '.*']; | ||
} | ||
// Select the first item from the database - only used by models. | ||
first: Promise.method(function() { | ||
this.query.where(this.syncing.format( | ||
_.extend(Object.create(null), this.syncing.attributes)) | ||
).limit(1); | ||
return this.select(); | ||
}), | ||
// Set the query builder on the options, in-case we need to | ||
// access in the `fetching` event handlers. | ||
options.query = this.query; | ||
// Runs a `select` query on the database, adding any necessary relational | ||
// constraints, resetting the query when complete. If there are results and | ||
// eager loaded relations, those are fetched and returned on the model before | ||
// the promise is resolved. Any `success` handler passed in the | ||
// options will be called - used by both models & collections. | ||
select: Promise.method(function() { | ||
var columns, sync = this, | ||
options = this.options, relatedData = this.syncing.relatedData; | ||
// Trigger a `fetching` event on the model, and then select the appropriate columns. | ||
return Promise.bind(this).then(function() { | ||
return this.syncing.triggerThen('fetching', this.syncing, columns, options); | ||
}).then(function() { | ||
return this.query.select(columns); | ||
}); | ||
}), | ||
// Inject all appropriate select costraints dealing with the relation | ||
// into the `knex` query builder for the current instance. | ||
if (relatedData) { | ||
relatedData.selectConstraints(this.query, options); | ||
} else { | ||
columns = options.columns; | ||
if (!_.isArray(columns)) columns = columns ? [columns] : [_.result(this.syncing, 'tableName') + '.*']; | ||
} | ||
// Issues an `insert` command on the query - only used by models. | ||
insert: Promise.method(function() { | ||
var syncing = this.syncing; | ||
return this.query | ||
.insert(syncing.format(_.extend(Object.create(null), syncing.attributes)), syncing.idAttribute); | ||
}), | ||
// Set the query builder on the options, in-case we need to | ||
// access in the `fetching` event handlers. | ||
options.query = this.query; | ||
// Issues an `update` command on the query - only used by models. | ||
update: Promise.method(function(attrs) { | ||
var syncing = this.syncing, query = this.query; | ||
if (syncing.id != null) query.where(syncing.idAttribute, syncing.id); | ||
if (query.wheres.length === 0) { | ||
throw new Error('A model cannot be updated without a "where" clause or an idAttribute.'); | ||
} | ||
return query.update(syncing.format(_.extend(Object.create(null), attrs))); | ||
}), | ||
// Trigger a `fetching` event on the model, and then select the appropriate columns. | ||
return Promise.bind(this).then(function() { | ||
return this.syncing.triggerThen('fetching', this.syncing, columns, options); | ||
}).then(function() { | ||
return this.query.select(columns); | ||
}); | ||
}), | ||
// Issues a `delete` command on the query. | ||
del: Promise.method(function() { | ||
var query = this.query, syncing = this.syncing; | ||
if (syncing.id != null) query.where(syncing.idAttribute, syncing.id); | ||
if (query.wheres.length === 0) { | ||
throw new Error('A model cannot be destroyed without a "where" clause or an idAttribute.'); | ||
} | ||
return this.query.del(); | ||
}) | ||
// Issues an `insert` command on the query - only used by models. | ||
insert: Promise.method(function() { | ||
var syncing = this.syncing; | ||
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; | ||
if (syncing.id != null) query.where(syncing.idAttribute, syncing.id); | ||
if (query.wheres.length === 0) { | ||
throw new Error('A model cannot be updated without a "where" clause or an idAttribute.'); | ||
} | ||
return query.update(syncing.format(_.extend(Object.create(null), attrs))); | ||
}), | ||
// Issues a `delete` command on the query. | ||
del: Promise.method(function() { | ||
var query = this.query, syncing = this.syncing; | ||
if (syncing.id != null) query.where(syncing.idAttribute, syncing.id); | ||
if (query.wheres.length === 0) { | ||
throw new Error('A model cannot be destroyed without a "where" clause or an idAttribute.'); | ||
} | ||
return this.query.del(); | ||
}) | ||
}); | ||
exports.Sync = Sync; | ||
}); | ||
})( | ||
typeof define === 'function' && define.amd ? define : function (factory) { factory(require, exports); } | ||
); | ||
exports.Sync = Sync; |
{ | ||
"name": "bookshelf", | ||
"version": "0.6.1", | ||
"version": "0.6.2", | ||
"description": "A lightweight ORM for PostgreSQL, MySQL, and SQLite3, influenced by Backbone.js", | ||
@@ -28,4 +28,4 @@ "main": "bookshelf.js", | ||
"knex": "~0.5.0", | ||
"bluebird": "~0.10.5-0", | ||
"lodash": "~2.3.0" | ||
"bluebird": ">=0.11.0", | ||
"lodash": ">=2.0.0" | ||
}, | ||
@@ -45,3 +45,4 @@ "devDependencies": { | ||
"sinon-chai": "~2.4.0", | ||
"sinon": "~1.7.3" | ||
"sinon": "~1.7.3", | ||
"node-uuid": "~1.4.1" | ||
}, | ||
@@ -48,0 +49,0 @@ "author": { |
@@ -1,63 +0,5 @@ | ||
// Exec plugin | ||
// Exec plugin (deprecated) | ||
// --------------- | ||
(function(define) { "use strict"; | ||
// The `exec` plugin is used to optionally add | ||
// support node-style callbacks, delegating to the promise | ||
// method under the hood: | ||
// `Bookshelf.plugin(require('bookshelf/plugins/exec'))` | ||
define(function(require, exports, module) { | ||
var _ = require('lodash'); | ||
// Accept the instance of `Bookshelf` we'd like to add `exec` support to. | ||
module.exports = function(Bookshelf) { | ||
// A method which is passed the `target` object and `method` we're | ||
// looking to extend with the `exec` interface. | ||
var wrapExec = function(target, method) { | ||
var targetMethod = target[method]; | ||
target[method] = function() { | ||
var result, args = arguments; | ||
var ctx = this; | ||
return { | ||
// The then method is essentially the same as it was before, | ||
// just is not automatically called. | ||
then: function(onFulfilled, onRejected) { | ||
result || (result = targetMethod.apply(ctx, args)); | ||
return result.then(onFulfilled, onRejected); | ||
}, | ||
// A facade for the `then` method, throwing any uncaught errors | ||
// rather than swallowing them. | ||
exec: function(callback) { | ||
result || (result = targetMethod.apply(ctx, args)); | ||
return result.then(function(resp) { | ||
callback(null, resp); | ||
}, function(err) { | ||
callback(err, null); | ||
}).then(null, function(err) { | ||
setTimeout(function() { throw err; }, 0); | ||
}); | ||
} | ||
}; | ||
}; | ||
}; | ||
// Wrap the appropriate methods on each object prototype, exposing the new API. | ||
_.each(['load', 'fetch', 'save', 'destroy'], function(method) { | ||
wrapExec(Bookshelf.Model.prototype, method); | ||
}); | ||
_.each(['load', 'fetch'], function(method) { | ||
wrapExec(Bookshelf.Collection.prototype, method); | ||
}); | ||
}; | ||
}); | ||
})( | ||
typeof define === 'function' && define.amd ? define : function (factory) { factory(require, exports, module); } | ||
); | ||
module.exports = function(Bookshelf) { | ||
console.log('This plugin is deprecated, `exec` is now callable from all sync methods'); | ||
}; |
@@ -66,3 +66,2 @@ var _ = require('lodash'); | ||
require('./integration/relations')(bookshelf); | ||
require('./integration/plugins')(bookshelf); | ||
}); | ||
@@ -69,0 +68,0 @@ |
@@ -9,3 +9,3 @@ var _ = require('lodash'); | ||
'users', 'roles', 'photos', 'users_roles', 'info', | ||
'Customer', 'Settings', 'hostnames', 'instances' | ||
'Customer', 'Settings', 'hostnames', 'instances', 'uuid_test' | ||
]; | ||
@@ -143,2 +143,7 @@ | ||
table.string('name'); | ||
}), | ||
schema.createTable('uuid_test', function(table) { | ||
table.uuid('uuid'); | ||
table.string('name'); | ||
}) | ||
@@ -145,0 +150,0 @@ |
@@ -28,2 +28,7 @@ | ||
var Uuid = Bookshelf.Model.extend({ | ||
idAttribute: 'uuid', | ||
tableName: 'uuid_test' | ||
}); | ||
var Site = Bookshelf.Model.extend({ | ||
@@ -258,3 +263,4 @@ tableName: 'sites', | ||
Instance: Instance, | ||
Hostname: Hostname | ||
Hostname: Hostname, | ||
Uuid: Uuid | ||
}, | ||
@@ -261,0 +267,0 @@ Collections: { |
@@ -1,2 +0,4 @@ | ||
var _ = require('lodash'); | ||
var _ = require('lodash'); | ||
var uuid = require('node-uuid'); | ||
_.str = require('underscore.string'); | ||
@@ -19,3 +21,3 @@ | ||
insert: function() { return Promise.resolve({}); }, | ||
update: function() { return Promise.resolve({}); }, | ||
update: function() { return Promise.resolve(1); }, | ||
del: function() { return Promise.resolve({}); } | ||
@@ -319,2 +321,16 @@ }; | ||
it('errors if the row was not updated', function() { | ||
return new Site({id: 200, name: 'This doesnt exist'}).save().then(function() { | ||
throw new Error('This should not succeed'); | ||
}, function(err) { | ||
expect(err.message).to.equal('No rows were affected in the update, did you mean to pass the {insert: true} option?'); | ||
}); | ||
}); | ||
it('should not error if updated row was not affected', function() { | ||
return new Site({id: 5, name: 'Fifth site, explicity created'}).save(); | ||
}); | ||
it('does not constrain on the `id` during update unless defined', function() { | ||
@@ -326,3 +342,3 @@ | ||
equal(this.wheres.length, 1); | ||
return Promise.resolve({}); | ||
return Promise.resolve(1); | ||
}; | ||
@@ -352,3 +368,3 @@ | ||
equal(this.wheres.length, 1); | ||
return Promise.resolve(this.toString()).then(onFulfilled, onRejected); | ||
return Promise.resolve(1).then(onFulfilled, onRejected); | ||
}; | ||
@@ -388,2 +404,25 @@ | ||
it('Allows setting a uuid, #24 #130', function() { | ||
var uuidval = uuid.v4(); | ||
var SubSite = Models.Uuid.extend({ | ||
initialize: function() { | ||
this.on('saving', this._generateId); | ||
}, | ||
_generateId: function (model, attrs, options) { | ||
if (model.isNew()) { | ||
model.set(model.idAttribute, uuidval); | ||
} | ||
} | ||
}); | ||
var subsite = new SubSite({name: 'testing'}); | ||
return subsite.save().then(function(model) { | ||
expect(model.id).to.equal(uuidval); | ||
expect(model.get('name')).to.equal('testing'); | ||
}).then(function() { | ||
return new SubSite({uuid: uuidval}).fetch(); | ||
}).then(function(model) { | ||
expect(model.get('name')).to.equal('testing'); | ||
}); | ||
}); | ||
}); | ||
@@ -390,0 +429,0 @@ |
@@ -203,3 +203,3 @@ var _ = require('lodash'); | ||
before(function() { | ||
beforeEach(function() { | ||
return Promise.all([ | ||
@@ -268,2 +268,59 @@ new Site({id: 1}).admins().detach(), | ||
it('keeps the attach method for eager loaded relations, #120', function() { | ||
var site1 = new Site({id: 1}); | ||
var site2 = new Site({id: 2}); | ||
var admin1 = new Admin({username: 'syncable', password: 'test'}); | ||
var admin2 = new Admin({username: 'syncable', password: 'test'}); | ||
var admin1_id; | ||
return Promise.all([admin1.save(), admin2.save(), | ||
site1.fetch({withRelated: 'admins'}), site2.fetch({withRelated: 'admins'})]) | ||
.then(function() { | ||
admin1_id = admin1.id; | ||
return Promise.all([ | ||
site1.related('admins').attach([admin1, admin2]), | ||
site2.related('admins').attach(admin2) | ||
]); | ||
}) | ||
.then(function(resp) { | ||
expect(site1.related('admins')).to.have.length(2); | ||
expect(site2.related('admins')).to.have.length(1); | ||
}).then(function() { | ||
return Promise.all([ | ||
new Site({id: 1}).related('admins').fetch().then(function(c) { | ||
c.each(function(m) { | ||
equal(m.hasChanged(), false); | ||
}); | ||
equal(c.at(0).pivot.get('item'), 'test'); | ||
equal(c.length, 2); | ||
}), | ||
new Site({id: 2}).related('admins').fetch().then(function(c) { | ||
equal(c.length, 1); | ||
}) | ||
]); | ||
}) | ||
.then(function(resp) { | ||
return Promise.all([ | ||
new Site({id: 1}).related('admins').fetch(), | ||
new Site({id: 2}).related('admins').fetch() | ||
]); | ||
}) | ||
.spread(function(admins1, admins2) { | ||
return Promise.all([ | ||
admins1.detach(admin1_id).then(function(c) { | ||
expect(admins1).to.have.length(1); | ||
return c.fetch(); | ||
}).then(function(c) { | ||
equal(c.length, 1); | ||
}), | ||
admins2.detach().then(function(c) { | ||
expect(admins2).to.have.length(0); | ||
return c.fetch(); | ||
}).then(function(c) { | ||
equal(c.length, 0); | ||
}) | ||
]); | ||
}); | ||
}); | ||
}); | ||
@@ -270,0 +327,0 @@ |
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
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
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
8
252659
14
47
6955
+ Addedbluebird@3.7.2(transitive)
- Removedbluebird@0.10.5-0(transitive)
- Removedlodash@2.3.0(transitive)
Updatedbluebird@>=0.11.0
Updatedlodash@>=2.0.0