Socket
Socket
Sign inDemoInstall

bookshelf

Package Overview
Dependencies
Maintainers
1
Versions
87
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

bookshelf - npm Package Compare versions

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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc