Socket
Socket
Sign inDemoInstall

backbone-relational

Package Overview
Dependencies
2
Maintainers
1
Versions
11
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 0.7.0 to 0.8.0

1377

backbone-relational.js

@@ -1,5 +0,5 @@

/* vim: set tabstop=4:softtabstop=4:shiftwidth=4:noexpandtab */
/* vim: set tabstop=4 softtabstop=4 shiftwidth=4 noexpandtab: */
/**
* Backbone-relational.js 0.7.0
* (c) 2011-2013 Paul Uithol
* Backbone-relational.js 0.8.0
* (c) 2011-2013 Paul Uithol and contributors (https://github.com/PaulUithol/Backbone-relational/graphs/contributors)
*

@@ -12,3 +12,3 @@ * Backbone-relational may be freely distributed under the MIT license; see the accompanying LICENSE.txt.

"use strict";
/**

@@ -39,3 +39,3 @@ * CommonJS shim

_permitsUsed: 0,
acquire: function() {

@@ -49,3 +49,3 @@ if ( this._permitsAvailable && this._permitsUsed >= this._permitsAvailable ) {

},
release: function() {

@@ -59,7 +59,7 @@ if ( this._permitsUsed === 0 ) {

},
isLocked: function() {
return this._permitsUsed > 0;
},
setAvailablePermits: function( amount ) {

@@ -72,3 +72,3 @@ if ( this._permitsUsed > amount ) {

};
/**

@@ -84,3 +84,3 @@ * A BlockingQueue that accumulates items while blocked (via 'block'),

_queue: null,
add: function( func ) {

@@ -94,3 +94,3 @@ if ( this.isBlocked() ) {

},
process: function() {

@@ -101,7 +101,7 @@ while ( this._queue && this._queue.length ) {

},
block: function() {
this.acquire();
},
unblock: function() {

@@ -113,3 +113,3 @@ this.release();

},
isBlocked: function() {

@@ -120,7 +120,7 @@ return this.isLocked();

/**
* Global event queue. Accumulates external events ('add:<key>', 'remove:<key>' and 'update:<key>')
* Global event queue. Accumulates external events ('add:<key>', 'remove:<key>' and 'change:<key>')
* until the top-level object is fully initialized (see 'Backbone.RelationalModel').
*/
Backbone.Relational.eventQueue = new Backbone.BlockingQueue();
/**

@@ -133,2 +133,3 @@ * Backbone.Store keeps track of all created (and destruction of) Backbone.RelationalModel.

this._reverseRelations = [];
this._orphanRelations = [];
this._subModels = [];

@@ -138,2 +139,22 @@ this._modelScopes = [ exports ];

_.extend( Backbone.Store.prototype, Backbone.Events, {
/**
* Create a new `Relation`.
* @param {Backbone.RelationalModel} [model]
* @param {Object} relation
* @param {Object} [options]
*/
initializeRelation: function( model, relation, options ) {
var type = !_.isString( relation.type ) ? relation.type : Backbone[ relation.type ] || this.getObjectByName( relation.type );
if ( type && type.prototype instanceof Backbone.Relation ) {
new type( model, relation, options ); // Also pushes the new Relation into `model._relations`
}
else {
Backbone.Relational.showWarnings && typeof console !== 'undefined' && console.warn( 'Relation=%o; missing or invalid relation type!', relation );
}
},
/**
* Add a scope for `getObjectByName` to look for model types by name.
* @param {Object} scope
*/
addModelScope: function( scope ) {

@@ -164,3 +185,3 @@ this._modelScopes.push( scope );

setupSuperModel: function( modelType ) {
_.find( this._subModels || [], function( subModelDef ) {
_.find( this._subModels, function( subModelDef ) {
return _.find( subModelDef.subModels || [], function( subModelTypeName, typeValue ) {

@@ -182,3 +203,3 @@ var subModelType = this.getObjectByName( subModelTypeName );

},
/**

@@ -194,29 +215,64 @@ * Add a reverse relation. Is added to the 'relations' property on model's prototype, and to

addReverseRelation: function( relation ) {
var exists = _.any( this._reverseRelations || [], function( rel ) {
return _.all( relation || [], function( val, key ) {
return val === rel[ key ];
});
var exists = _.any( this._reverseRelations, function( rel ) {
return _.all( relation || [], function( val, key ) {
return val === rel[ key ];
});
});
if ( !exists && relation.model && relation.type ) {
this._reverseRelations.push( relation );
var addRelation = function( model, relation ) {
if ( !model.prototype.relations ) {
model.prototype.relations = [];
}
model.prototype.relations.push( relation );
_.each( model._subModels || [], function( subModel ) {
addRelation( subModel, relation );
}, this );
};
addRelation( relation.model, relation );
this._addRelation( relation.model, relation );
this.retroFitRelation( relation );
}
},
/**
* Deposit a `relation` for which the `relatedModel` can't be resolved at the moment.
*
* @param {Object} relation
*/
addOrphanRelation: function( relation ) {
var exists = _.any( this._orphanRelations, function( rel ) {
return _.all( relation || [], function( val, key ) {
return val === rel[ key ];
});
});
if ( !exists && relation.model && relation.type ) {
this._orphanRelations.push( relation );
}
},
/**
* Try to initialize any `_orphanRelation`s
*/
processOrphanRelations: function() {
// Make sure to operate on a copy since we're removing while iterating
_.each( this._orphanRelations.slice( 0 ), function( rel ) {
var relatedModel = Backbone.Relational.store.getObjectByName( rel.relatedModel );
if ( relatedModel ) {
this.initializeRelation( null, rel );
this._orphanRelations = _.without( this._orphanRelations, rel );
}
}, this );
},
/**
*
* @param {Backbone.RelationalModel.constructor} type
* @param {Object} relation
* @private
*/
_addRelation: function( type, relation ) {
if ( !type.prototype.relations ) {
type.prototype.relations = [];
}
type.prototype.relations.push( relation );
_.each( type._subModels || [], function( subModel ) {
this._addRelation( subModel, relation );
}, this );
},
/**
* Add a 'relation' to all existing instances of 'relation.model' in the store

@@ -226,4 +282,4 @@ * @param {Object} relation

retroFitRelation: function( relation ) {
var coll = this.getCollection( relation.model );
coll.each( function( model ) {
var coll = this.getCollection( relation.model, false );
coll && coll.each( function( model ) {
if ( !( model instanceof relation.model ) ) {

@@ -234,16 +290,17 @@ return;

new relation.type( model, relation );
}, this);
}, this );
},
/**
* Find the Store's collection for a certain type of model.
* @param {Backbone.RelationalModel} model
* @param {Backbone.RelationalModel} type
* @param {Boolean} [create=true] Should a collection be created if none is found?
* @return {Backbone.Collection} A collection if found (or applicable for 'model'), or null
*/
getCollection: function( model ) {
if ( model instanceof Backbone.RelationalModel ) {
model = model.constructor;
getCollection: function( type, create ) {
if ( type instanceof Backbone.RelationalModel ) {
type = type.constructor;
}
var rootModel = model;
var rootModel = type;
while ( rootModel._superModel ) {

@@ -253,7 +310,5 @@ rootModel = rootModel._superModel;

var coll = _.detect( this._collections, function( c ) {
return c.model === rootModel;
});
var coll = _.findWhere( this._collections, { model: rootModel } );
if ( !coll ) {
if ( !coll && create !== false ) {
coll = this._createCollection( rootModel );

@@ -264,5 +319,5 @@ }

},
/**
* Find a type on the global object by name. Splits name on dots.
* Find a model type on one of the modelScopes by name. Names are split on dots.
* @param {String} name

@@ -275,3 +330,3 @@ * @return {Object}

_.find( this._modelScopes || [], function( scope ) {
_.find( this._modelScopes, function( scope ) {
type = _.reduce( parts || [], function( memo, val ) {

@@ -288,3 +343,3 @@ return memo ? memo[ val ] : undefined;

},
_createCollection: function( type ) {

@@ -356,5 +411,5 @@ var coll;

},
/**
* Add a 'model' to it's appropriate collection. Retain the original contents of 'model.collection'.
* Add a 'model' to its appropriate collection. Retain the original contents of 'model.collection'.
* @param {Backbone.RelationalModel} model

@@ -367,2 +422,5 @@ */

if ( coll.get( model ) ) {
if ( Backbone.Relational.showWarnings && typeof console !== 'undefined' ) {
console.warn( 'Duplicate id! Old RelationalModel=%o, new RelationalModel=%o', coll.get( model ), model );
}
throw new Error( "Cannot instantiate more than one Backbone.RelationalModel with the same id per type!" );

@@ -373,11 +431,11 @@ }

coll.add( model );
model.bind( 'destroy', this.unregister, this );
this.listenTo( model, 'destroy', this.unregister, this );
model.collection = modelColl;
}
},
/**
* Explicitly update a model's id in it's store collection
* Explicitly update a model's id in its store collection
* @param {Backbone.RelationalModel} model
*/
*/
update: function( model ) {

@@ -387,3 +445,3 @@ var coll = this.getCollection( model );

},
/**

@@ -394,9 +452,20 @@ * Remove a 'model' from the store.

unregister: function( model ) {
model.unbind( 'destroy', this.unregister );
this.stopListening( model, 'destroy', this.unregister );
var coll = this.getCollection( model );
coll && coll.remove( model );
},
/**
* Reset the `store` to it's original state. The `reverseRelations` are kept though, since attempting to
* re-initialize these on models would lead to a large amount of warnings.
*/
reset: function() {
this.stopListening();
this._collections = [];
this._subModels = [];
this._modelScopes = [ exports ];
}
});
Backbone.Relational.store = new Backbone.Store();
/**

@@ -406,3 +475,4 @@ * The main Relation class, from which 'HasOne' and 'HasMany' inherit. Internally, 'relational:<key>' events

*
* @param {Backbone.RelationalModel} instance
* @param {Backbone.RelationalModel} [instance] Model that this relation is created for. If no model is supplied,
* Relation just tries to instantiate it's `reverseRelation` if specified, and bails out after that.
* @param {Object} options

@@ -416,4 +486,5 @@ * @param {string} options.key

* {Backbone.Relation|String} type ('HasOne' or 'HasMany').
* @param {Object} opts
*/
Backbone.Relation = function( instance, options ) {
Backbone.Relation = function( instance, options, opts ) {
this.instance = instance;

@@ -423,7 +494,7 @@ // Make sure 'options' is sane, and fill with defaults from subclasses and this object's prototype

this.reverseRelation = _.defaults( options.reverseRelation || {}, this.options.reverseRelation );
this.options = _.defaults( options, this.options, Backbone.Relation.prototype.options );
this.reverseRelation.type = !_.isString( this.reverseRelation.type ) ? this.reverseRelation.type :
Backbone[ this.reverseRelation.type ] || Backbone.Relational.store.getObjectByName( this.reverseRelation.type );
this.model = options.model || this.instance.constructor;
this.options = _.defaults( options, this.options, Backbone.Relation.prototype.options );
this.key = this.options.key;

@@ -433,3 +504,3 @@ this.keySource = this.options.keySource || this.key;

// 'exports' should be the global object where 'relatedModel' can be found on if given as a string.
this.model = this.options.model || this.instance.constructor;
this.relatedModel = this.options.relatedModel;

@@ -444,2 +515,14 @@ if ( _.isString( this.relatedModel ) ) {

// Add the reverse relation on 'relatedModel' to the store's reverseRelations
if ( !this.options.isAutoRelation && this.reverseRelation.type && this.reverseRelation.key ) {
Backbone.Relational.store.addReverseRelation( _.defaults( {
isAutoRelation: true,
model: this.relatedModel,
relatedModel: this.model,
reverseRelation: this.options // current relation is the 'reverseRelation' for its own reverseRelation
},
this.reverseRelation // Take further properties from this.reverseRelation (type, key, etc.)
) );
}
if ( instance ) {

@@ -451,3 +534,4 @@ var contentKey = this.keySource;

this.keyContents = this.instance.get( contentKey );
this.setKeyContents( this.instance.get( contentKey ) );
this.relatedCollection = Backbone.Relational.store.getCollection( this.relatedModel );

@@ -460,34 +544,14 @@ // Explicitly clear 'keySource', to prevent a leaky abstraction if 'keySource' differs from 'key'.

// Add this Relation to instance._relations
this.instance._relations.push( this );
}
this.instance._relations[ this.key ] = this;
// Add the reverse relation on 'relatedModel' to the store's reverseRelations
if ( !this.options.isAutoRelation && this.reverseRelation.type && this.reverseRelation.key ) {
Backbone.Relational.store.addReverseRelation( _.defaults( {
isAutoRelation: true,
model: this.relatedModel,
relatedModel: this.model,
reverseRelation: this.options // current relation is the 'reverseRelation' for it's own reverseRelation
},
this.reverseRelation // Take further properties from this.reverseRelation (type, key, etc.)
) );
}
this.initialize( opts );
_.bindAll( this, '_modelRemovedFromCollection', '_relatedModelAdded', '_relatedModelRemoved' );
if ( instance ) {
this.initialize();
if ( options.autoFetch ) {
this.instance.fetchRelated( options.key, _.isObject( options.autoFetch ) ? options.autoFetch : {} );
if ( this.options.autoFetch ) {
this.instance.fetchRelated( this.key, _.isObject( this.options.autoFetch ) ? this.options.autoFetch : {} );
}
// When a model in the store is destroyed, check if it is 'this.instance'.
Backbone.Relational.store.getCollection( this.instance )
.bind( 'relational:remove', this._modelRemovedFromCollection );
// When 'relatedModel' are created or destroyed, check if it affects this relation.
Backbone.Relational.store.getCollection( this.relatedModel )
.bind( 'relational:add', this._relatedModelAdded )
.bind( 'relational:remove', this._relatedModelRemoved );
this.listenTo( this.instance, 'destroy', this.destroy )
.listenTo( this.relatedCollection, 'relational:add', this.tryAddRelated )
.listenTo( this.relatedCollection, 'relational:remove', this.removeRelated )
}

@@ -503,5 +567,6 @@ };

isAutoRelation: false,
autoFetch: false
autoFetch: false,
parse: false
},
instance: null,

@@ -511,24 +576,6 @@ key: null,

relatedModel: null,
relatedCollection: null,
reverseRelation: null,
related: null,
_relatedModelAdded: function( model, coll, options ) {
// Allow 'model' to set up it's relations, before calling 'tryAddRelated'
// (which can result in a call to 'addRelated' on a relation of 'model')
var dit = this;
model.queue( function() {
dit.tryAddRelated( model, options );
});
},
_relatedModelRemoved: function( model, coll, options ) {
this.removeRelated( model, options );
},
_modelRemovedFromCollection: function( model ) {
if ( model === this.instance ) {
this.destroy();
}
},
/**

@@ -546,3 +593,3 @@ * Check several pre-conditions.

if ( !m || !k || !rm ) {
warn && console.warn( 'Relation=%o; no model, key or relatedModel (%o, %o, %o)', this, m, k, rm );
warn && console.warn( 'Relation=%o: missing model, key or relatedModel (%o, %o, %o).', this, m, k, rm );
return false;

@@ -552,3 +599,3 @@ }

if ( !( m.prototype instanceof Backbone.RelationalModel ) ) {
warn && console.warn( 'Relation=%o; model does not inherit from Backbone.RelationalModel (%o)', this, i );
warn && console.warn( 'Relation=%o: model does not inherit from Backbone.RelationalModel (%o).', this, i );
return false;

@@ -558,3 +605,3 @@ }

if ( !( rm.prototype instanceof Backbone.RelationalModel ) ) {
warn && console.warn( 'Relation=%o; relatedModel does not inherit from Backbone.RelationalModel (%o)', this, rm );
warn && console.warn( 'Relation=%o: relatedModel does not inherit from Backbone.RelationalModel (%o).', this, rm );
return false;

@@ -564,17 +611,14 @@ }

if ( this instanceof Backbone.HasMany && this.reverseRelation.type === Backbone.HasMany ) {
warn && console.warn( 'Relation=%o; relation is a HasMany, and the reverseRelation is HasMany as well.', this );
warn && console.warn( 'Relation=%o: relation is a HasMany, and the reverseRelation is HasMany as well.', this );
return false;
}
// Check if we're not attempting to create a relationship on a `key` that's already used.
if ( i && _.keys( i._relations ).length ) {
var existing = _.find( i._relations, function( rel ) {
return rel.key === k;
}, this );
// Check if we're not attempting to create a duplicate relationship
if ( i && i._relations.length ) {
var exists = _.any( i._relations || [], function( rel ) {
var hasReverseRelation = this.reverseRelation.key && rel.reverseRelation.key;
return rel.relatedModel === rm && rel.key === k &&
( !hasReverseRelation || this.reverseRelation.key === rel.reverseRelation.key );
}, this );
if ( exists ) {
warn && console.warn( 'Relation=%o between instance=%o.%s and relatedModel=%o.%s already exists',
this, i, k, rm, this.reverseRelation.key );
if ( existing ) {
warn && console.warn( 'Cannot create relation=%o on %o for model=%o: already taken by relation=%o.',
this, k, i, existing );
return false;

@@ -590,12 +634,11 @@ }

* @param {Backbone.Model|Backbone.Collection} related
* @param {Object} [options]
*/
setRelated: function( related, options ) {
setRelated: function( related ) {
this.related = related;
this.instance.acquire();
this.instance.set( this.key, related, _.defaults( options || {}, { silent: true } ) );
this.instance.attributes[ this.key ] = related;
this.instance.release();
},
/**

@@ -608,9 +651,6 @@ * Determine if a relation (on a different RelationalModel) is the reverse

_isReverseRelation: function( relation ) {
if ( relation.instance instanceof this.relatedModel && this.reverseRelation.key === relation.key &&
this.key === relation.reverseRelation.key ) {
return true;
}
return false;
return relation.instance instanceof this.relatedModel && this.reverseRelation.key === relation.key &&
this.key === relation.reverseRelation.key;
},
/**

@@ -627,57 +667,32 @@ * Get the reverse relations (pointing back to 'this.key' on 'this.instance') for the currently related model(s).

_.each( models || [], function( related ) {
_.each( related.getRelations() || [], function( relation ) {
if ( this._isReverseRelation( relation ) ) {
reverseRelations.push( relation );
}
}, this );
}, this );
_.each( related.getRelations() || [], function( relation ) {
if ( this._isReverseRelation( relation ) ) {
reverseRelations.push( relation );
}
}, this );
}, this );
return reverseRelations;
},
/**
* Rename options.silent to options.silentChange, so events propagate properly.
* (for example in HasMany, from 'addRelated'->'handleAddition')
* @param {Object} [options]
* @return {Object}
* When `this.instance` is destroyed, cleanup our relations.
* Get reverse relation, call removeRelated on each.
*/
sanitizeOptions: function( options ) {
options = options ? _.clone( options ) : {};
if ( options.silent ) {
options.silentChange = true;
delete options.silent;
}
return options;
},
destroy: function() {
this.stopListening();
/**
* Rename options.silentChange to options.silent, so events are silenced as intended in Backbone's
* original functions.
* @param {Object} [options]
* @return {Object}
*/
unsanitizeOptions: function( options ) {
options = options ? _.clone( options ) : {};
if ( options.silentChange ) {
options.silent = true;
delete options.silentChange;
if ( this instanceof Backbone.HasOne ) {
this.setRelated( null );
}
return options;
},
// Cleanup. Get reverse relation, call removeRelated on each.
destroy: function() {
Backbone.Relational.store.getCollection( this.instance )
.unbind( 'relational:remove', this._modelRemovedFromCollection );
else if ( this instanceof Backbone.HasMany ) {
this.setRelated( this._prepareCollection() );
}
Backbone.Relational.store.getCollection( this.relatedModel )
.unbind( 'relational:add', this._relatedModelAdded )
.unbind( 'relational:remove', this._relatedModelRemoved );
_.each( this.getReverseRelations() || [], function( relation ) {
relation.removeRelated( this.instance );
}, this );
_.each( this.getReverseRelations(), function( relation ) {
relation.removeRelated( this.instance );
}, this );
}
});
Backbone.HasOne = Backbone.Relation.extend({

@@ -687,34 +702,49 @@ options: {

},
initialize: function() {
_.bindAll( this, 'onChange' );
this.instance.bind( 'relational:change:' + this.key, this.onChange );
initialize: function( opts ) {
this.listenTo( this.instance, 'relational:change:' + this.key, this.onChange );
var model = this.findRelated( { silent: true } );
this.setRelated( model );
var related = this.findRelated( opts );
this.setRelated( related );
// Notify new 'related' object of the new relation.
_.each( this.getReverseRelations() || [], function( relation ) {
relation.addRelated( this.instance );
}, this );
_.each( this.getReverseRelations(), function( relation ) {
relation.addRelated( this.instance, opts );
}, this );
},
/**
* Find related Models.
* @param {Object} [options]
* @return {Backbone.Model}
*/
findRelated: function( options ) {
var item = this.keyContents;
var model = null;
if ( item instanceof this.relatedModel ) {
model = item;
var related = null;
options = _.defaults( { parse: this.options.parse }, options );
if ( this.keyContents instanceof this.relatedModel ) {
related = this.keyContents;
}
else if ( item || item === 0 ) { // since 0 can be a valid `id` as well
model = this.relatedModel.findOrCreate( item, { create: this.options.createModels } );
else if ( this.keyContents || this.keyContents === 0 ) { // since 0 can be a valid `id` as well
var opts = _.defaults( { create: this.options.createModels }, options );
related = this.relatedModel.findOrCreate( this.keyContents, opts );
}
return model;
return related;
},
/**
* If the key is changed, notify old & new reverse relations and initialize the new relation
* Normalize and reduce `keyContents` to an `id`, for easier comparison
* @param {String|Number|Backbone.Model} keyContents
*/
setKeyContents: function( keyContents ) {
this.keyContents = keyContents;
this.keyId = Backbone.Relational.store.resolveIdForItem( this.relatedModel, this.keyContents );
},
/**
* Event handler for `change:<key>`.
* If the key is changed, notify old & new reverse relations and initialize the new relation.
*/
onChange: function( model, attr, options ) {

@@ -726,24 +756,14 @@ // Don't accept recursive calls to onChange (like onChange->findRelated->findOrCreate->initializeRelations->addRelated->onChange)

this.acquire();
options = this.sanitizeOptions( options );
options = options ? _.clone( options ) : {};
// 'options._related' is set by 'addRelated'/'removeRelated'. If it is set, the change
// 'options.__related' is set by 'addRelated'/'removeRelated'. If it is set, the change
// is the result of a call from a relation. If it's not, the change is the result of
// a 'set' call on this.instance.
var changed = _.isUndefined( options._related );
var oldRelated = changed ? this.related : options._related;
var changed = _.isUndefined( options.__related ),
oldRelated = changed ? this.related : options.__related;
if ( changed ) {
this.keyContents = attr;
// Set new 'related'
if ( attr instanceof this.relatedModel ) {
this.related = attr;
}
else if ( attr ) {
var related = this.findRelated( options );
this.setRelated( related );
}
else {
this.setRelated( null );
}
if ( changed ) {
this.setKeyContents( attr );
var related = this.findRelated( options );
this.setRelated( related );
}

@@ -753,19 +773,21 @@

if ( oldRelated && this.related !== oldRelated ) {
_.each( this.getReverseRelations( oldRelated ) || [], function( relation ) {
relation.removeRelated( this.instance, options );
}, this );
_.each( this.getReverseRelations( oldRelated ), function( relation ) {
relation.removeRelated( this.instance, null, options );
}, this );
}
// Notify new 'related' object of the new relation. Note we do re-apply even if this.related is oldRelated;
// that can be necessary for bi-directional relations if 'this.instance' was created after 'this.related'.
// In that case, 'this.instance' will already know 'this.related', but the reverse might not exist yet.
_.each( this.getReverseRelations() || [], function( relation ) {
relation.addRelated( this.instance, options );
}, this);
_.each( this.getReverseRelations(), function( relation ) {
relation.addRelated( this.instance, options );
}, this );
// Fire the 'update:<key>' event if 'related' was updated
if ( !options.silentChange && this.related !== oldRelated ) {
// Fire the 'change:<key>' event if 'related' was updated
if ( !options.silent && this.related !== oldRelated ) {
var dit = this;
this.changed = true;
Backbone.Relational.eventQueue.add( function() {
dit.instance.trigger( 'update:' + dit.key, dit.instance, dit.related, options );
dit.instance.trigger( 'change:' + dit.key, dit.instance, dit.related, options, true );
dit.changed = false;
});

@@ -775,30 +797,27 @@ }

},
/**
* If a new 'this.relatedModel' appears in the 'store', try to match it to the last set 'keyContents'
*/
tryAddRelated: function( model, options ) {
if ( this.related ) {
return;
tryAddRelated: function( model, coll, options ) {
if ( ( this.keyId || this.keyId === 0 ) && model.id === this.keyId ) { // since 0 can be a valid `id` as well
this.addRelated( model, options );
this.keyId = null;
}
options = this.sanitizeOptions( options );
var item = this.keyContents;
if ( item || item === 0 ) { // since 0 can be a valid `id` as well
var id = Backbone.Relational.store.resolveIdForItem( this.relatedModel, item );
if ( !_.isNull( id ) && model.id === id ) {
this.addRelated( model, options );
}
}
},
addRelated: function( model, options ) {
if ( model !== this.related ) {
var oldRelated = this.related || null;
this.setRelated( model );
this.onChange( this.instance, model, { _related: oldRelated } );
}
// Allow 'model' to set up its relations before proceeding.
// (which can result in a call to 'addRelated' from a relation of 'model')
var dit = this;
model.queue( function() {
if ( model !== dit.related ) {
var oldRelated = dit.related || null;
dit.setRelated( model );
dit.onChange( dit.instance, model, _.defaults( { __related: oldRelated }, options ) );
}
});
},
removeRelated: function( model, options ) {
removeRelated: function( model, coll, options ) {
if ( !this.related ) {

@@ -811,10 +830,10 @@ return;

this.setRelated( null );
this.onChange( this.instance, model, { _related: oldRelated } );
this.onChange( this.instance, model, _.defaults( { __related: oldRelated }, options ) );
}
}
});
Backbone.HasMany = Backbone.Relation.extend({
collectionType: null,
options: {

@@ -826,6 +845,5 @@ reverseRelation: { type: 'HasOne' },

},
initialize: function() {
_.bindAll( this, 'onChange', 'handleAddition', 'handleRemoval', 'handleReset' );
this.instance.bind( 'relational:change:' + this.key, this.onChange );
initialize: function( opts ) {
this.listenTo( this.instance, 'relational:change:' + this.key, this.onChange );

@@ -838,21 +856,8 @@ // Handle a custom 'collectionType'

if ( !this.collectionType.prototype instanceof Backbone.Collection ){
throw new Error( 'collectionType must inherit from Backbone.Collection' );
throw new Error( '`collectionType` must inherit from Backbone.Collection' );
}
// Handle cases where a model/relation is created with a collection passed straight into 'attributes'
if ( this.keyContents instanceof Backbone.Collection ) {
this.setRelated( this._prepareCollection( this.keyContents ) );
}
else {
this.setRelated( this._prepareCollection() );
}
this.findRelated( { silent: true } );
var related = this.findRelated( opts );
this.setRelated( related );
},
_getCollectionOptions: function() {
return _.isFunction( this.options.collectionOptions ) ?
this.options.collectionOptions( this.instance ) :
this.options.collectionOptions;
},

@@ -863,13 +868,14 @@ /**

* @param {Backbone.Collection} [collection]
* @return {Backbone.Collection}
*/
_prepareCollection: function( collection ) {
if ( this.related ) {
this.related
.unbind( 'relational:add', this.handleAddition )
.unbind( 'relational:remove', this.handleRemoval )
.unbind( 'relational:reset', this.handleReset )
this.stopListening( this.related );
}
if ( !collection || !( collection instanceof Backbone.Collection ) ) {
collection = new this.collectionType( [], this._getCollectionOptions() );
var options = _.isFunction( this.options.collectionOptions ) ?
this.options.collectionOptions( this.instance ) : this.options.collectionOptions;
collection = new this.collectionType( null, options );
}

@@ -891,144 +897,120 @@

}
this.listenTo( collection, 'relational:add', this.handleAddition )
.listenTo( collection, 'relational:remove', this.handleRemoval )
.listenTo( collection, 'relational:reset', this.handleReset );
collection
.bind( 'relational:add', this.handleAddition )
.bind( 'relational:remove', this.handleRemoval )
.bind( 'relational:reset', this.handleReset );
return collection;
},
/**
* Find related Models.
* @param {Object} [options]
* @return {Backbone.Collection}
*/
findRelated: function( options ) {
if ( this.keyContents ) {
var models = [];
var related = null;
if ( this.keyContents instanceof Backbone.Collection ) {
models = this.keyContents.models;
options = _.defaults( { parse: this.options.parse }, options );
// Replace 'this.related' by 'this.keyContents' if it is a Backbone.Collection
if ( this.keyContents instanceof Backbone.Collection ) {
this._prepareCollection( this.keyContents );
related = this.keyContents;
}
// Otherwise, 'this.keyContents' should be an array of related object ids.
// Re-use the current 'this.related' if it is a Backbone.Collection; otherwise, create a new collection.
else {
var toAdd = [];
_.each( this.keyContents, function( attributes ) {
if ( attributes instanceof this.relatedModel ) {
var model = attributes;
}
else {
// If `merge` is true, update models here, instead of during update.
model = this.relatedModel.findOrCreate( attributes, _.extend( { merge: true }, options, { create: this.options.createModels } ) );
}
model && toAdd.push( model );
}, this );
if ( this.related instanceof Backbone.Collection ) {
related = this.related;
}
else {
// Handle cases the an API/user supplies just an Object/id instead of an Array
this.keyContents = _.isArray( this.keyContents ) ? this.keyContents : [ this.keyContents ];
related = this._prepareCollection();
}
// Try to find instances of the appropriate 'relatedModel' in the store
_.each( this.keyContents || [], function( item ) {
var model = null;
if ( item instanceof this.relatedModel ) {
model = item;
}
else if ( item || item === 0 ) { // since 0 can be a valid `id` as well
model = this.relatedModel.findOrCreate( item, { create: this.options.createModels } );
}
related.update( toAdd, _.defaults( { merge: false, parse: false }, options ) );
}
if ( model && !this.related.get( model ) ) {
models.push( model );
}
}, this );
}
return related;
},
// Add all found 'models' in on go, so 'add' will only be called once (and thus 'sort', etc.)
if ( models.length ) {
options = this.unsanitizeOptions( options );
this.related.add( models, options );
}
/**
* Normalize and reduce `keyContents` to a list of `ids`, for easier comparison
* @param {String|Number|String[]|Number[]|Backbone.Collection} keyContents
*/
setKeyContents: function( keyContents ) {
this.keyContents = keyContents instanceof Backbone.Collection ? keyContents : null;
this.keyIds = [];
if ( !this.keyContents && ( keyContents || keyContents === 0 ) ) { // since 0 can be a valid `id` as well
// Handle cases the an API/user supplies just an Object/id instead of an Array
this.keyContents = _.isArray( keyContents ) ? keyContents : [ keyContents ];
_.each( this.keyContents, function( item ) {
var itemId = Backbone.Relational.store.resolveIdForItem( this.relatedModel, item );
if ( itemId || itemId === 0 ) {
this.keyIds.push( itemId );
}
}, this );
}
},
/**
* If the key is changed, notify old & new reverse relations and initialize the new relation
* Event handler for `change:<key>`.
* If the contents of the key are changed, notify old & new reverse relations and initialize the new relation.
*/
onChange: function( model, attr, options ) {
options = this.sanitizeOptions( options );
this.keyContents = attr;
// Replace 'this.related' by 'attr' if it is a Backbone.Collection
if ( attr instanceof Backbone.Collection ) {
this._prepareCollection( attr );
this.related = attr;
}
// Otherwise, 'attr' should be an array of related object ids.
// Re-use the current 'this.related' if it is a Backbone.Collection, and remove any current entries.
// Otherwise, create a new collection.
else {
var oldIds = {}, newIds = {};
options = options ? _.clone( options ) : {};
this.setKeyContents( attr );
this.changed = false;
if (!_.isArray( attr ) && attr !== undefined) {
attr = [ attr ];
}
var related = this.findRelated( options );
this.setRelated( related );
_.each( attr, function( attributes ) {
newIds[ attributes.id ] = true;
if ( !options.silent ) {
var dit = this;
Backbone.Relational.eventQueue.add( function() {
// The `changed` flag can be set in `handleAddition` or `handleRemoval`
if ( dit.changed ) {
dit.instance.trigger( 'change:' + dit.key, dit.instance, dit.related, options, true );
dit.changed = false;
}
});
var coll = this.related;
if ( coll instanceof Backbone.Collection ) {
// Make sure to operate on a copy since we're removing while iterating
_.each( coll.models.slice(0) , function( model ) {
// When fetch is called with the 'keepNewModels' option, we don't want to remove
// client-created new models when the fetch is completed.
if ( !options.keepNewModels || !model.isNew() ) {
oldIds[ model.id ] = true;
coll.remove( model, { silent: (model.id in newIds) } );
}
});
} else {
coll = this._prepareCollection();
}
_.each( attr, function( attributes ) {
var model = this.relatedModel.findOrCreate( attributes, { create: this.options.createModels } );
if (model) {
coll.add( model, { silent: (attributes.id in oldIds)} );
}
}, this);
this.setRelated( coll );
}
var dit = this;
Backbone.Relational.eventQueue.add( function() {
!options.silentChange && dit.instance.trigger( 'update:' + dit.key, dit.instance, dit.related, options );
});
},
tryAddRelated: function( model, options ) {
options = this.sanitizeOptions( options );
if ( !this.related.get( model ) ) {
// Check if this new model was specified in 'this.keyContents'
var item = _.any( this.keyContents || [], function( item ) {
var id = Backbone.Relational.store.resolveIdForItem( this.relatedModel, item );
return !_.isNull( id ) && id === model.id;
}, this );
if ( item ) {
this.related.add( model, options );
}
}
},
/**
* When a model is added to a 'HasMany', trigger 'add' on 'this.instance' and notify reverse relations.
* (should be 'HasOne', must set 'this.instance' as their related).
*/
*/
handleAddition: function( model, coll, options ) {
//console.debug('handleAddition called; args=%o', arguments);
// Make sure the model is in fact a valid model before continuing.
// (it can be invalid as a result of failing validation in Backbone.Collection._prepareModel)
if ( !( model instanceof Backbone.Model ) ) {
return;
}
options = options ? _.clone( options ) : {};
this.changed = true;
options = this.sanitizeOptions( options );
_.each( this.getReverseRelations( model ) || [], function( relation ) {
relation.addRelated( this.instance, options );
}, this );
_.each( this.getReverseRelations( model ), function( relation ) {
relation.addRelated( this.instance, options );
}, this );
// Only trigger 'add' once the newly added model is initialized (so, has it's relations set up)
// Only trigger 'add' once the newly added model is initialized (so, has its relations set up)
var dit = this;
Backbone.Relational.eventQueue.add( function() {
!options.silentChange && dit.instance.trigger( 'add:' + dit.key, model, dit.related, options );
!options.silent && Backbone.Relational.eventQueue.add( function() {
dit.instance.trigger( 'add:' + dit.key, model, dit.related, options );
});
},
/**

@@ -1040,15 +1022,12 @@ * When a model is removed from a 'HasMany', trigger 'remove' on 'this.instance' and notify reverse relations.

//console.debug('handleRemoval called; args=%o', arguments);
if ( !( model instanceof Backbone.Model ) ) {
return;
}
options = this.sanitizeOptions( options );
options = options ? _.clone( options ) : {};
this.changed = true;
_.each( this.getReverseRelations( model ) || [], function( relation ) {
relation.removeRelated( this.instance, options );
}, this );
_.each( this.getReverseRelations( model ), function( relation ) {
relation.removeRelated( this.instance, null, options );
}, this );
var dit = this;
Backbone.Relational.eventQueue.add( function() {
!options.silentChange && dit.instance.trigger( 'remove:' + dit.key, model, dit.related, options );
!options.silent && Backbone.Relational.eventQueue.add( function() {
dit.instance.trigger( 'remove:' + dit.key, model, dit.related, options );
});

@@ -1058,14 +1037,23 @@ },

handleReset: function( coll, options ) {
options = this.sanitizeOptions( options );
var dit = this;
Backbone.Relational.eventQueue.add( function() {
!options.silentChange && dit.instance.trigger( 'reset:' + dit.key, dit.related, options );
options = options ? _.clone( options ) : {};
!options.silent && Backbone.Relational.eventQueue.add( function() {
dit.instance.trigger( 'reset:' + dit.key, dit.related, options );
});
},
tryAddRelated: function( model, coll, options ) {
var item = _.contains( this.keyIds, model.id );
if ( item ) {
this.addRelated( model, options );
this.keyIds = _.without( this.keyIds, model.id );
}
},
addRelated: function( model, options ) {
// Allow 'model' to set up its relations before proceeding.
// (which can result in a call to 'addRelated' from a relation of 'model')
var dit = this;
options = this.unsanitizeOptions( options );
model.queue( function() { // Queued to avoid errors for adding 'model' to the 'this.related' set twice
model.queue( function() {
if ( dit.related && !dit.related.get( model ) ) {

@@ -1076,5 +1064,4 @@ dit.related.add( model, options );

},
removeRelated: function( model, options ) {
options = this.unsanitizeOptions( options );
removeRelated: function( model, coll, options ) {
if ( this.related.get( model ) ) {

@@ -1085,3 +1072,3 @@ this.related.remove( model, options );

});
/**

@@ -1092,3 +1079,3 @@ * A type of Backbone.Model that also maintains relations to other models and collections.

* - 'remove:<key>' (model, related collection, options)
* - 'update:<key>' (model, related model or collection, options)
* - 'change:<key>' (model, related model or collection, options)
*/

@@ -1101,18 +1088,20 @@ Backbone.RelationalModel = Backbone.Model.extend({

_queue: null,
subModelTypeAttribute: 'type',
subModelTypes: null,
constructor: function( attributes, options ) {
// Nasty hack, for cases like 'model.get( <HasMany key> ).add( item )'.
// Defer 'processQueue', so that when 'Relation.createModels' is used we:
// a) Survive 'Backbone.Collection.add'; this takes care we won't error on "can't add model to a set twice"
// (by creating a model from properties, having the model add itself to the collection via one of
// it's relations, then trying to add it to the collection).
// b) Trigger 'HasMany' collection events only after the model is really fully set up.
// Example that triggers both a and b: "p.get('jobs').add( { company: c, person: p } )".
var dit = this;
// Defer 'processQueue', so that when 'Relation.createModels' is used we trigger 'HasMany'
// collection events only after the model is really fully set up.
// Example: "p.get('jobs').add( { company: c, person: p } )".
if ( options && options.collection ) {
var dit = this,
collection = this.collection = options.collection;
// Prevent this option from cascading down to related models; they shouldn't go into this `if` clause.
delete options.collection;
this._deferProcessing = true;
var processQueue = function( model ) {

@@ -1122,8 +1111,8 @@ if ( model === dit ) {

dit.processQueue();
options.collection.unbind( 'relational:add', processQueue );
collection.off( 'relational:add', processQueue );
}
};
options.collection.bind( 'relational:add', processQueue );
// So we do process the queue eventually, regardless of whether this model really gets added to 'options.collection'.
collection.on( 'relational:add', processQueue );
// So we do process the queue eventually, regardless of whether this model actually gets added to 'options.collection'.
_.defer( function() {

@@ -1133,2 +1122,4 @@ processQueue( dit );

}
Backbone.Relational.store.processOrphanRelations();

@@ -1138,9 +1129,12 @@ this._queue = new Backbone.BlockingQueue();

Backbone.Relational.eventQueue.block();
Backbone.Model.apply( this, arguments );
// Try to run the global queue holding external events
Backbone.Relational.eventQueue.unblock();
try {
Backbone.Model.apply( this, arguments );
}
finally {
// Try to run the global queue holding external events
Backbone.Relational.eventQueue.unblock();
}
},
/**

@@ -1150,7 +1144,42 @@ * Override 'trigger' to queue 'change' and 'change:*' events

trigger: function( eventName ) {
if ( eventName.length > 5 && 'change' === eventName.substr( 0, 6 ) ) {
var dit = this, args = arguments;
if ( eventName.length > 5 && eventName.indexOf( 'change' ) === 0 ) {
var dit = this,
args = arguments;
Backbone.Relational.eventQueue.add( function() {
Backbone.Model.prototype.trigger.apply( dit, args );
});
if ( !dit._isInitialized ) {
return;
}
// Determine if the `change` event is still valid, now that all relations are populated
var changed = true;
if ( eventName === 'change' ) {
changed = dit.hasChanged();
}
else {
var attr = eventName.slice( 7 ),
rel = dit.getRelation( attr );
if ( rel ) {
// If `attr` is a relation, `change:attr` get triggered from `Relation.onChange`.
// These take precedence over `change:attr` events triggered by `Model.set`.
// The relation set a fourth attribute to `true`. If this attribute is present,
// continue triggering this event; otherwise, it's from `Model.set` and should be stopped.
changed = ( args[ 4 ] === true );
// If this event was triggered by a relation, set the right value in `this.changed`
// (a Collection or Model instead of raw data).
if ( changed ) {
dit.changed[ attr ] = args[ 2 ];
}
// Otherwise, this event is from `Model.set`. If the relation doesn't report a change,
// remove attr from `dit.changed` so `hasChanged` doesn't take it into account.
else if ( !rel.changed ) {
delete dit.changed[ attr ];
}
}
}
changed && Backbone.Model.prototype.trigger.apply( dit, args );
});
}

@@ -1163,3 +1192,3 @@ else {

},
/**

@@ -1169,15 +1198,9 @@ * Initialize Relations present in this.relations; determine the type (HasOne/HasMany), then creates a new instance.

*/
initializeRelations: function() {
initializeRelations: function( options ) {
this.acquire(); // Setting up relations often also involve calls to 'set', and we only want to enter this function once
this._relations = [];
this._relations = {};
_.each( this.relations || [], function( rel ) {
var type = !_.isString( rel.type ) ? rel.type : Backbone[ rel.type ] || Backbone.Relational.store.getObjectByName( rel.type );
if ( type && type.prototype instanceof Backbone.Relation ) {
new type( this, rel ); // Also pushes the new Relation into _relations
}
else {
Backbone.Relational.showWarnings && typeof console !== 'undefined' && console.warn( 'Relation=%o; missing or invalid type!', rel );
}
}, this );
Backbone.Relational.store.initializeRelation( this, rel, options );
}, this );

@@ -1195,3 +1218,3 @@ this._isInitialized = true;

if ( this._isInitialized && !this.isLocked() ) {
_.each( this._relations || [], function( rel ) {
_.each( this._relations, function( rel ) {
// Update from data in `rel.keySource` if set, or `rel.key` otherwise

@@ -1205,3 +1228,3 @@ var val = this.attributes[ rel.keySource ] || this.attributes[ rel.key ];

},
/**

@@ -1213,3 +1236,3 @@ * Either add to the queue (if we're not initialized yet), or execute right away.

},
/**

@@ -1223,3 +1246,3 @@ * Process _queue

},
/**

@@ -1231,9 +1254,5 @@ * Get a specific relation.

getRelation: function( key ) {
return _.detect( this._relations, function( rel ) {
if ( rel.key === key ) {
return true;
}
}, this );
return this._relations[ key ];
},
/**

@@ -1244,5 +1263,5 @@ * Get all of the created relations.

getRelations: function() {
return this._relations;
return _.values( this._relations );
},
/**

@@ -1252,32 +1271,32 @@ * Retrieve related objects.

* @param [options] {Object} Options for 'Backbone.Model.fetch' and 'Backbone.sync'.
* @param [update=false] {boolean} Whether to force a fetch from the server (updating existing models).
* @param [refresh=false] {boolean} Fetch existing models from the server as well (in order to update them).
* @return {jQuery.when[]} An array of request objects
*/
fetchRelated: function( key, options, update ) {
options || ( options = {} );
fetchRelated: function( key, options, refresh ) {
// Set default `options` for fetch
options = _.extend( { update: true, remove: false }, options );
var setUrl,
requests = [],
rel = this.getRelation( key ),
keyContents = rel && rel.keyContents,
toFetch = keyContents && _.select( _.isArray( keyContents ) ? keyContents : [ keyContents ], function( item ) {
var id = Backbone.Relational.store.resolveIdForItem( rel.relatedModel, item );
return !_.isNull( id ) && ( update || !Backbone.Relational.store.find( rel.relatedModel, id ) );
keys = rel && ( rel.keyIds || [ rel.keyId ] ),
toFetch = keys && _.select( keys || [], function( id ) {
return ( id || id === 0 ) && ( refresh || !Backbone.Relational.store.find( rel.relatedModel, id ) );
}, this );
if ( toFetch && toFetch.length ) {
// Create a model for each entry in 'keyContents' that is to be fetched
var models = _.map( toFetch, function( item ) {
var model;
// Find (or create) a model for each one that is to be fetched
var created = [],
models = _.map( toFetch, function( id ) {
var model = Backbone.Relational.store.find( rel.relatedModel, id );
if ( !model ) {
var attrs = {};
attrs[ rel.relatedModel.prototype.idAttribute ] = id;
model = rel.relatedModel.findOrCreate( attrs, options );
created.push( model );
}
if ( _.isObject( item ) ) {
model = rel.relatedModel.findOrCreate( item );
}
else {
var attrs = {};
attrs[ rel.relatedModel.prototype.idAttribute ] = item;
model = rel.relatedModel.findOrCreate( attrs );
}
return model;
}, this );
return model;
}, this );

@@ -1297,11 +1316,10 @@ // Try if the 'collection' can provide a url to fetch a set of models in one request.

var args = arguments;
_.each( models || [], function( model ) {
model.trigger( 'destroy', model, model.collection, options );
options.error && options.error.apply( model, args );
});
_.each( created, function( model ) {
model.trigger( 'destroy', model, model.collection, options );
options.error && options.error.apply( model, args );
});
},
url: setUrl
},
options,
{ add: true }
options
);

@@ -1312,8 +1330,10 @@

else {
requests = _.map( models || [], function( model ) {
requests = _.map( models, function( model ) {
var opts = _.defaults(
{
error: function() {
model.trigger( 'destroy', model, model.collection, options );
options.error && options.error.apply( model, arguments );
if ( _.contains( created, model ) ) {
model.trigger( 'destroy', model, model.collection, options );
options.error && options.error.apply( model, arguments );
}
}

@@ -1330,3 +1350,28 @@ },

},
get: function( attr ) {
var originalResult = Backbone.Model.prototype.get.call( this, attr );
// Use `originalResult` get if dotNotation not enabled or not required because no dot is in `attr`
if ( !this.dotNotation || attr.indexOf( '.' ) === -1 ) {
return originalResult;
}
// Go through all splits and return the final result
var splits = attr.split( '.' );
var result = _.reduce(splits, function( model, split ) {
if ( !( model instanceof Backbone.Model ) ) {
throw new Error( 'Attribute must be an instanceof Backbone.Model. Is: ' + model + ', currentSplit: ' + split );
}
return Backbone.Model.prototype.get.call( model, split );
}, this );
if ( originalResult !== undefined && result !== undefined ) {
throw new Error( "Ambiguous result for '" + attr + "'. direct result: " + originalResult + ", dotNotation: " + result );
}
return originalResult || result;
},
set: function( key, value, options ) {

@@ -1349,24 +1394,27 @@ Backbone.Relational.eventQueue.block();

// Ideal place to set up relations :)
if ( !this._isInitialized && !this.isLocked() ) {
this.constructor.initializeModelHierarchy();
try {
if ( !this._isInitialized && !this.isLocked() ) {
this.constructor.initializeModelHierarchy();
Backbone.Relational.store.register( this );
Backbone.Relational.store.register( this );
this.initializeRelations();
this.initializeRelations( options );
}
// Update the 'idAttribute' in Backbone.store if; we don't want it to miss an 'id' update due to {silent:true}
else if ( attributes && this.idAttribute in attributes ) {
Backbone.Relational.store.update( this );
}
if ( attributes ) {
this.updateRelations( options );
}
}
// Update the 'idAttribute' in Backbone.store if; we don't want it to miss an 'id' update due to {silent:true}
else if ( attributes && this.idAttribute in attributes ) {
Backbone.Relational.store.update( this );
finally {
// Try to run the global queue holding external events
Backbone.Relational.eventQueue.unblock();
}
if ( attributes ) {
this.updateRelations( options );
}
// Try to run the global queue holding external events
Backbone.Relational.eventQueue.unblock();
return result;
},
unset: function( attribute, options ) {

@@ -1383,3 +1431,3 @@ Backbone.Relational.eventQueue.block();

},
clear: function( options ) {

@@ -1396,13 +1444,2 @@ Backbone.Relational.eventQueue.block();

},
/**
* Override 'change', so the change will only execute after 'set' has finised (relations are updated),
* and 'previousAttributes' will be available when the event is fired.
*/
change: function( options ) {
var dit = this, args = arguments;
Backbone.Relational.eventQueue.add( function() {
Backbone.Model.prototype.change.apply( dit, args );
});
},

@@ -1415,13 +1452,13 @@ clone: function() {

_.each( this.getRelations() || [], function( rel ) {
delete attributes[ rel.key ];
});
_.each( this.getRelations(), function( rel ) {
delete attributes[ rel.key ];
});
return new this.constructor( attributes );
},
/**
* Convert relations to JSON, omits them when required
*/
toJSON: function(options) {
toJSON: function( options ) {
// If this Model has already been fully serialized in this branch once, return to avoid loops

@@ -1439,55 +1476,55 @@ if ( this.isLocked() ) {

_.each( this._relations || [], function( rel ) {
var value = json[ rel.key ];
_.each( this._relations, function( rel ) {
var value = json[ rel.key ];
if ( rel.options.includeInJSON === true) {
if ( value && _.isFunction( value.toJSON ) ) {
json[ rel.keyDestination ] = value.toJSON( options );
}
else {
json[ rel.keyDestination ] = null;
}
if ( rel.options.includeInJSON === true) {
if ( value && _.isFunction( value.toJSON ) ) {
json[ rel.keyDestination ] = value.toJSON( options );
}
else if ( _.isString( rel.options.includeInJSON ) ) {
if ( value instanceof Backbone.Collection ) {
json[ rel.keyDestination ] = value.pluck( rel.options.includeInJSON );
}
else if ( value instanceof Backbone.Model ) {
json[ rel.keyDestination ] = value.get( rel.options.includeInJSON );
}
else {
json[ rel.keyDestination ] = null;
}
else {
json[ rel.keyDestination ] = null;
}
else if ( _.isArray( rel.options.includeInJSON ) ) {
if ( value instanceof Backbone.Collection ) {
var valueSub = [];
value.each( function( model ) {
var curJson = {};
_.each( rel.options.includeInJSON, function( key ) {
curJson[ key ] = model.get( key );
});
valueSub.push( curJson );
});
json[ rel.keyDestination ] = valueSub;
}
else if ( value instanceof Backbone.Model ) {
var valueSub = {};
}
else if ( _.isString( rel.options.includeInJSON ) ) {
if ( value instanceof Backbone.Collection ) {
json[ rel.keyDestination ] = value.pluck( rel.options.includeInJSON );
}
else if ( value instanceof Backbone.Model ) {
json[ rel.keyDestination ] = value.get( rel.options.includeInJSON );
}
else {
json[ rel.keyDestination ] = null;
}
}
else if ( _.isArray( rel.options.includeInJSON ) ) {
if ( value instanceof Backbone.Collection ) {
var valueSub = [];
value.each( function( model ) {
var curJson = {};
_.each( rel.options.includeInJSON, function( key ) {
valueSub[ key ] = value.get( key );
curJson[ key ] = model.get( key );
});
json[ rel.keyDestination ] = valueSub;
}
else {
json[ rel.keyDestination ] = null;
}
valueSub.push( curJson );
});
json[ rel.keyDestination ] = valueSub;
}
else if ( value instanceof Backbone.Model ) {
var valueSub = {};
_.each( rel.options.includeInJSON, function( key ) {
valueSub[ key ] = value.get( key );
});
json[ rel.keyDestination ] = valueSub;
}
else {
delete json[ rel.key ];
json[ rel.keyDestination ] = null;
}
}
else {
delete json[ rel.key ];
}
if ( rel.keyDestination !== rel.key ) {
delete json[ rel.key ];
}
});
if ( rel.keyDestination !== rel.key ) {
delete json[ rel.key ];
}
});

@@ -1499,2 +1536,7 @@ this.release();

{
/**
*
* @param superModel
* @returns {Backbone.RelationalModel.constructor}
*/
setup: function( superModel ) {

@@ -1519,25 +1561,30 @@ // We don't want to share a relations array with a parent, as this will cause problems with

_.each( this.prototype.relations || [], function( rel ) {
if ( !rel.model ) {
rel.model = this;
if ( !rel.model ) {
rel.model = this;
}
if ( rel.reverseRelation && rel.model === this ) {
var preInitialize = true;
if ( _.isString( rel.relatedModel ) ) {
/**
* The related model might not be defined for two reasons
* 1. it is related to itself
* 2. it never gets defined, e.g. a typo
* 3. the model hasn't been defined yet, but will be later
* In neither of these cases do we need to pre-initialize reverse relations.
* However, for 3. (which is, to us, indistinguishable from 2.), we do need to attempt
* setting up this relation again later, in case the related model is defined later.
*/
var relatedModel = Backbone.Relational.store.getObjectByName( rel.relatedModel );
preInitialize = relatedModel && ( relatedModel.prototype instanceof Backbone.RelationalModel );
}
if ( rel.reverseRelation && rel.model === this ) {
var preInitialize = true;
if ( _.isString( rel.relatedModel ) ) {
/**
* The related model might not be defined for two reasons
* 1. it never gets defined, e.g. a typo
* 2. it is related to itself
* In neither of these cases do we need to pre-initialize reverse relations.
*/
var relatedModel = Backbone.Relational.store.getObjectByName( rel.relatedModel );
preInitialize = relatedModel && ( relatedModel.prototype instanceof Backbone.RelationalModel );
}
var type = !_.isString( rel.type ) ? rel.type : Backbone[ rel.type ] || Backbone.Relational.store.getObjectByName( rel.type );
if ( preInitialize && type && type.prototype instanceof Backbone.Relation ) {
new type( null, rel );
}
if ( preInitialize ) {
Backbone.Relational.store.initializeRelation( null, rel );
}
}, this );
else if ( _.isString( rel.relatedModel ) ) {
Backbone.Relational.store.addOrphanRelation( rel );
}
}
}, this );

@@ -1572,2 +1619,5 @@ return this;

/**
*
*/
initializeModelHierarchy: function() {

@@ -1611,3 +1661,3 @@ // If we're here for the first time, try to determine if this modelType has a 'superModel'.

* - If `attributes` is a string or a number, `findOrCreate` will just query the `store` and return a model if found.
* - If `attributes` is an object, the model will be updated with `attributes` if found.
* - If `attributes` is an object and is found in the store, the model will be updated with `attributes` unless `options.update` is `false`.
* Otherwise, a new model is created with `attributes` (unless `options.create` is explicitly set to `false`).

@@ -1617,16 +1667,21 @@ * @param {Object|String|Number} attributes Either a model's id, or the attributes used to create or update a model.

* @param {Boolean} [options.create=true]
* @param {Boolean} [options.merge=true]
* @param {Boolean} [options.parse=false]
* @return {Backbone.RelationalModel}
*/
findOrCreate: function( attributes, options ) {
var parsedAttributes = (_.isObject( attributes ) && this.prototype.parse) ? this.prototype.parse( attributes ) : attributes;
options || ( options = {} );
var parsedAttributes = ( _.isObject( attributes ) && options.parse && this.prototype.parse ) ?
this.prototype.parse( attributes ) : attributes;
// Try to find an instance of 'this' model type in the store
var model = Backbone.Relational.store.find( this, parsedAttributes );
// If we found an instance, update it with the data in 'item'; if not, create an instance
// (unless 'options.create' is false).
// If we found an instance, update it with the data in 'item' (unless 'options.merge' is false).
// If not, create an instance (unless 'options.create' is false).
if ( _.isObject( attributes ) ) {
if ( model ) {
if ( model && options.merge !== false ) {
model.set( parsedAttributes, options );
}
else if ( !options || ( options && options.create !== false ) ) {
else if ( !model && options.create !== false ) {
model = this.build( attributes, options );

@@ -1640,6 +1695,8 @@ }

_.extend( Backbone.RelationalModel.prototype, Backbone.Semaphore );
/**
* Override Backbone.Collection._prepareModel, so objects will be built using the correct type
* if the collection.model has subModels.
* Attempts to find a model for `attrs` in Backbone.store through `findOrCreate`
* (which sets the new properties on it if found), or instantiates a new model.
*/

@@ -1667,3 +1724,4 @@ Backbone.Collection.prototype.__prepareModel = Backbone.Collection.prototype._prepareModel;

if ( !model._validate( attrs, options ) ) {
if ( model && model.isNew() && !model._validate( attrs, options ) ) {
this.trigger( 'invalid', this, attrs, options );
model = false;

@@ -1676,41 +1734,54 @@ }

/**
* Override Backbone.Collection.add, so objects fetched from the server multiple times will
* update the existing Model. Also, trigger 'relational:add'.
* Override Backbone.Collection.add, so we'll create objects from attributes where required,
* and update the existing models. Also, trigger 'relational:add'.
*/
var add = Backbone.Collection.prototype.__add = Backbone.Collection.prototype.add;
Backbone.Collection.prototype.add = function( models, options ) {
options || (options = {});
if ( !_.isArray( models ) ) {
models = [ models ];
// Short-circuit if this Collection doesn't hold RelationalModels
if ( !( this.model.prototype instanceof Backbone.RelationalModel ) ) {
return add.apply( this, arguments );
}
var modelsToAdd = [];
models = _.isArray( models ) ? models.slice() : [ models ];
// Set default options to the same values as `add` uses, so `findOrCreate` will also respect those.
options = _.extend( { merge: false }, options );
var newModels = [],
toAdd = [];
//console.debug( 'calling add on coll=%o; model=%o, options=%o', this, models, options );
_.each( models || [], function( model ) {
_.each( models, function( model ) {
if ( !( model instanceof Backbone.Model ) ) {
// `_prepareModel` attempts to find `model` in Backbone.store through `findOrCreate`,
// and sets the new properties on it if is found. Otherwise, a new model is instantiated.
model = Backbone.Collection.prototype._prepareModel.call( this, model, options );
}
if ( model instanceof Backbone.Model && !this.get( model ) ) {
modelsToAdd.push( model );
if ( model ) {
toAdd.push( model );
if ( !( this.get( model ) || this.get( model.cid ) ) ) {
newModels.push( model );
}
}, this );
// If we arrive in `add` while performing a `set` (after a create, so the model gains an `id`),
// we may get here before `_onModelEvent` has had the chance to update `_byId`.
else if ( model.id != null ) {
this._byId[ model.id ] = model;
}
}
}, this );
// Add 'models' in a single batch, so the original add will only be called once (and thus 'sort', etc).
if ( modelsToAdd.length ) {
add.call( this, modelsToAdd, options );
add.call( this, toAdd, options );
_.each( modelsToAdd || [], function( model ) {
_.each( newModels, function( model ) {
// Fire a `relational:add` event for any model in `newModels` that has actually been added to the collection.
if ( this.get( model ) || this.get( model.cid ) ) {
this.trigger( 'relational:add', model, this, options );
}, this );
}
}
}, this );
return this;
};
/**

@@ -1721,19 +1792,25 @@ * Override 'Backbone.Collection.remove' to trigger 'relational:remove'.

Backbone.Collection.prototype.remove = function( models, options ) {
options || (options = {});
if ( !_.isArray( models ) ) {
models = [ models ];
// Short-circuit if this Collection doesn't hold RelationalModels
if ( !( this.model.prototype instanceof Backbone.RelationalModel ) ) {
return remove.apply( this, arguments );
}
else {
models = models.slice( 0 );
}
models = _.isArray( models ) ? models.slice() : [ models ];
options || ( options = {} );
var toRemove = [];
//console.debug('calling remove on coll=%o; models=%o, options=%o', this, models, options );
_.each( models || [], function( model ) {
model = this.get( model );
_.each( models, function( model ) {
model = this.get( model ) || this.get( model.cid );
model && toRemove.push( model );
}, this );
if ( model instanceof Backbone.Model ) {
remove.call( this, model, options );
this.trigger('relational:remove', model, this, options);
}
if ( toRemove.length ) {
remove.call( this, toRemove, options );
_.each( toRemove, function( model ) {
this.trigger('relational:remove', model, this, options);
}, this );
}

@@ -1749,4 +1826,7 @@ return this;

reset.call( this, models, options );
this.trigger( 'relational:reset', this, options );
if ( this.model.prototype instanceof Backbone.RelationalModel ) {
this.trigger( 'relational:reset', this, options );
}
return this;

@@ -1761,7 +1841,10 @@ };

sort.call( this, options );
this.trigger( 'relational:reset', this, options );
if ( this.model.prototype instanceof Backbone.RelationalModel ) {
this.trigger( 'relational:reset', this, options );
}
return this;
};
/**

@@ -1773,17 +1856,21 @@ * Override 'Backbone.Collection.trigger' so 'add', 'remove' and 'reset' events are queued until relations

Backbone.Collection.prototype.trigger = function( eventName ) {
// Short-circuit if this Collection doesn't hold RelationalModels
if ( !( this.model.prototype instanceof Backbone.RelationalModel ) ) {
return trigger.apply( this, arguments );
}
if ( eventName === 'add' || eventName === 'remove' || eventName === 'reset' ) {
var dit = this, args = arguments;
var dit = this,
args = arguments;
if (eventName === 'add') {
if ( _.isObject( args[ 3 ] ) ) {
args = _.toArray( args );
// the fourth argument in case of a regular add is the option object.
// the fourth argument is the option object.
// we need to clone it, as it could be modified while we wait on the eventQueue to be unblocked
if (_.isObject( args[3] ) ) {
args[3] = _.clone( args[3] );
}
args[ 3 ] = _.clone( args[ 3 ] );
}
Backbone.Relational.eventQueue.add( function() {
trigger.apply( dit, args );
});
trigger.apply( dit, args );
});
}

@@ -1790,0 +1877,0 @@ else {

/**
* Backbone-relational.js 0.7.0
* (c) 2011-2013 Paul Uithol
* Backbone-relational.js 0.8.0
* (c) 2011-2013 Paul Uithol and contributors (https://github.com/PaulUithol/Backbone-relational/graphs/contributors)
*

@@ -9,2 +9,2 @@ * Backbone-relational may be freely distributed under the MIT license; see the accompanying LICENSE.txt.

*/
(function(a){"use strict";var b,c,d;"undefined"==typeof window?(b=require("underscore"),c=require("backbone"),d=module.exports=c):(b=window._,c=window.Backbone,d=window),c.Relational={showWarnings:!0},c.Semaphore={_permitsAvailable:null,_permitsUsed:0,acquire:function(){if(this._permitsAvailable&&this._permitsUsed>=this._permitsAvailable)throw Error("Max permits acquired");this._permitsUsed++},release:function(){if(0===this._permitsUsed)throw Error("All permits released");this._permitsUsed--},isLocked:function(){return this._permitsUsed>0},setAvailablePermits:function(a){if(this._permitsUsed>a)throw Error("Available permits cannot be less than used permits");this._permitsAvailable=a}},c.BlockingQueue=function(){this._queue=[]},b.extend(c.BlockingQueue.prototype,c.Semaphore,{_queue:null,add:function(a){this.isBlocked()?this._queue.push(a):a()},process:function(){for(;this._queue&&this._queue.length;)this._queue.shift()()},block:function(){this.acquire()},unblock:function(){this.release(),this.isBlocked()||this.process()},isBlocked:function(){return this.isLocked()}}),c.Relational.eventQueue=new c.BlockingQueue,c.Store=function(){this._collections=[],this._reverseRelations=[],this._subModels=[],this._modelScopes=[d]},b.extend(c.Store.prototype,c.Events,{addModelScope:function(a){this._modelScopes.push(a)},addSubModels:function(a,b){this._subModels.push({superModelType:b,subModels:a})},setupSuperModel:function(c){b.find(this._subModels||[],function(d){return b.find(d.subModels||[],function(b,e){var f=this.getObjectByName(b);return c===f?(d.superModelType._subModels[e]=c,c._superModel=d.superModelType,c._subModelTypeValue=e,c._subModelTypeAttribute=d.superModelType.prototype.subModelTypeAttribute,!0):a},this)},this)},addReverseRelation:function(a){var c=b.any(this._reverseRelations||[],function(c){return b.all(a||[],function(a,b){return a===c[b]})});if(!c&&a.model&&a.type){this._reverseRelations.push(a);var d=function(a,c){a.prototype.relations||(a.prototype.relations=[]),a.prototype.relations.push(c),b.each(a._subModels||[],function(a){d(a,c)},this)};d(a.model,a),this.retroFitRelation(a)}},retroFitRelation:function(a){var b=this.getCollection(a.model);b.each(function(b){b instanceof a.model&&new a.type(b,a)},this)},getCollection:function(a){a instanceof c.RelationalModel&&(a=a.constructor);for(var d=a;d._superModel;)d=d._superModel;var e=b.detect(this._collections,function(a){return a.model===d});return e||(e=this._createCollection(d)),e},getObjectByName:function(c){var d=c.split("."),e=null;return b.find(this._modelScopes||[],function(c){return e=b.reduce(d||[],function(b,c){return b?b[c]:a},c),e&&e!==c?!0:a},this),e},_createCollection:function(a){var b;return a instanceof c.RelationalModel&&(a=a.constructor),a.prototype instanceof c.RelationalModel&&(b=new c.Collection,b.model=a,this._collections.push(b)),b},resolveIdForItem:function(a,d){var e=b.isString(d)||b.isNumber(d)?d:null;return null===e&&(d instanceof c.RelationalModel?e=d.id:b.isObject(d)&&(e=d[a.prototype.idAttribute])),e||0===e||(e=null),e},find:function(a,b){var c=this.resolveIdForItem(a,b),d=this.getCollection(a);if(d){var e=d.get(c);if(e instanceof a)return e}return null},register:function(a){var b=this.getCollection(a);if(b){if(b.get(a))throw Error("Cannot instantiate more than one Backbone.RelationalModel with the same id per type!");var c=a.collection;b.add(a),a.bind("destroy",this.unregister,this),a.collection=c}},update:function(a){var b=this.getCollection(a);b._onModelEvent("change:"+a.idAttribute,a,b)},unregister:function(a){a.unbind("destroy",this.unregister);var b=this.getCollection(a);b&&b.remove(a)}}),c.Relational.store=new c.Store,c.Relation=function(a,d){if(this.instance=a,d=b.isObject(d)?d:{},this.reverseRelation=b.defaults(d.reverseRelation||{},this.options.reverseRelation),this.reverseRelation.type=b.isString(this.reverseRelation.type)?c[this.reverseRelation.type]||c.Relational.store.getObjectByName(this.reverseRelation.type):this.reverseRelation.type,this.model=d.model||this.instance.constructor,this.options=b.defaults(d,this.options,c.Relation.prototype.options),this.key=this.options.key,this.keySource=this.options.keySource||this.key,this.keyDestination=this.options.keyDestination||this.keySource||this.key,this.relatedModel=this.options.relatedModel,b.isString(this.relatedModel)&&(this.relatedModel=c.Relational.store.getObjectByName(this.relatedModel)),this.checkPreconditions()){if(a){var e=this.keySource;e!==this.key&&"object"==typeof this.instance.get(this.key)&&(e=this.key),this.keyContents=this.instance.get(e),this.keySource!==this.key&&this.instance.unset(this.keySource,{silent:!0}),this.instance._relations.push(this)}!this.options.isAutoRelation&&this.reverseRelation.type&&this.reverseRelation.key&&c.Relational.store.addReverseRelation(b.defaults({isAutoRelation:!0,model:this.relatedModel,relatedModel:this.model,reverseRelation:this.options},this.reverseRelation)),b.bindAll(this,"_modelRemovedFromCollection","_relatedModelAdded","_relatedModelRemoved"),a&&(this.initialize(),d.autoFetch&&this.instance.fetchRelated(d.key,b.isObject(d.autoFetch)?d.autoFetch:{}),c.Relational.store.getCollection(this.instance).bind("relational:remove",this._modelRemovedFromCollection),c.Relational.store.getCollection(this.relatedModel).bind("relational:add",this._relatedModelAdded).bind("relational:remove",this._relatedModelRemoved))}},c.Relation.extend=c.Model.extend,b.extend(c.Relation.prototype,c.Events,c.Semaphore,{options:{createModels:!0,includeInJSON:!0,isAutoRelation:!1,autoFetch:!1},instance:null,key:null,keyContents:null,relatedModel:null,reverseRelation:null,related:null,_relatedModelAdded:function(a,b,c){var d=this;a.queue(function(){d.tryAddRelated(a,c)})},_relatedModelRemoved:function(a,b,c){this.removeRelated(a,c)},_modelRemovedFromCollection:function(a){a===this.instance&&this.destroy()},checkPreconditions:function(){var a=this.instance,d=this.key,e=this.model,f=this.relatedModel,g=c.Relational.showWarnings&&"undefined"!=typeof console;if(!e||!d||!f)return g&&console.warn("Relation=%o; no model, key or relatedModel (%o, %o, %o)",this,e,d,f),!1;if(!(e.prototype instanceof c.RelationalModel))return g&&console.warn("Relation=%o; model does not inherit from Backbone.RelationalModel (%o)",this,a),!1;if(!(f.prototype instanceof c.RelationalModel))return g&&console.warn("Relation=%o; relatedModel does not inherit from Backbone.RelationalModel (%o)",this,f),!1;if(this instanceof c.HasMany&&this.reverseRelation.type===c.HasMany)return g&&console.warn("Relation=%o; relation is a HasMany, and the reverseRelation is HasMany as well.",this),!1;if(a&&a._relations.length){var h=b.any(a._relations||[],function(a){var b=this.reverseRelation.key&&a.reverseRelation.key;return a.relatedModel===f&&a.key===d&&(!b||this.reverseRelation.key===a.reverseRelation.key)},this);if(h)return g&&console.warn("Relation=%o between instance=%o.%s and relatedModel=%o.%s already exists",this,a,d,f,this.reverseRelation.key),!1}return!0},setRelated:function(a,c){this.related=a,this.instance.acquire(),this.instance.set(this.key,a,b.defaults(c||{},{silent:!0})),this.instance.release()},_isReverseRelation:function(a){return a.instance instanceof this.relatedModel&&this.reverseRelation.key===a.key&&this.key===a.reverseRelation.key?!0:!1},getReverseRelations:function(a){var c=[],d=b.isUndefined(a)?this.related&&(this.related.models||[this.related]):[a];return b.each(d||[],function(a){b.each(a.getRelations()||[],function(a){this._isReverseRelation(a)&&c.push(a)},this)},this),c},sanitizeOptions:function(a){return a=a?b.clone(a):{},a.silent&&(a.silentChange=!0,delete a.silent),a},unsanitizeOptions:function(a){return a=a?b.clone(a):{},a.silentChange&&(a.silent=!0,delete a.silentChange),a},destroy:function(){c.Relational.store.getCollection(this.instance).unbind("relational:remove",this._modelRemovedFromCollection),c.Relational.store.getCollection(this.relatedModel).unbind("relational:add",this._relatedModelAdded).unbind("relational:remove",this._relatedModelRemoved),b.each(this.getReverseRelations()||[],function(a){a.removeRelated(this.instance)},this)}}),c.HasOne=c.Relation.extend({options:{reverseRelation:{type:"HasMany"}},initialize:function(){b.bindAll(this,"onChange"),this.instance.bind("relational:change:"+this.key,this.onChange);var a=this.findRelated({silent:!0});this.setRelated(a),b.each(this.getReverseRelations()||[],function(a){a.addRelated(this.instance)},this)},findRelated:function(){var b=this.keyContents,c=null;return b instanceof this.relatedModel?c=b:(b||0===b)&&(c=this.relatedModel.findOrCreate(b,{create:this.options.createModels})),c},onChange:function(a,d,e){if(!this.isLocked()){this.acquire(),e=this.sanitizeOptions(e);var f=b.isUndefined(e._related),g=f?this.related:e._related;if(f)if(this.keyContents=d,d instanceof this.relatedModel)this.related=d;else if(d){var h=this.findRelated(e);this.setRelated(h)}else this.setRelated(null);if(g&&this.related!==g&&b.each(this.getReverseRelations(g)||[],function(a){a.removeRelated(this.instance,e)},this),b.each(this.getReverseRelations()||[],function(a){a.addRelated(this.instance,e)},this),!e.silentChange&&this.related!==g){var i=this;c.Relational.eventQueue.add(function(){i.instance.trigger("update:"+i.key,i.instance,i.related,e)})}this.release()}},tryAddRelated:function(a,d){if(!this.related){d=this.sanitizeOptions(d);var e=this.keyContents;if(e||0===e){var f=c.Relational.store.resolveIdForItem(this.relatedModel,e);b.isNull(f)||a.id!==f||this.addRelated(a,d)}}},addRelated:function(a){if(a!==this.related){var c=this.related||null;this.setRelated(a),this.onChange(this.instance,a,{_related:c})}},removeRelated:function(a){if(this.related&&a===this.related){var c=this.related||null;this.setRelated(null),this.onChange(this.instance,a,{_related:c})}}}),c.HasMany=c.Relation.extend({collectionType:null,options:{reverseRelation:{type:"HasOne"},collectionType:c.Collection,collectionKey:!0,collectionOptions:{}},initialize:function(){if(b.bindAll(this,"onChange","handleAddition","handleRemoval","handleReset"),this.instance.bind("relational:change:"+this.key,this.onChange),this.collectionType=this.options.collectionType,b.isString(this.collectionType)&&(this.collectionType=c.Relational.store.getObjectByName(this.collectionType)),!this.collectionType.prototype instanceof c.Collection)throw Error("collectionType must inherit from Backbone.Collection");this.keyContents instanceof c.Collection?this.setRelated(this._prepareCollection(this.keyContents)):this.setRelated(this._prepareCollection()),this.findRelated({silent:!0})},_getCollectionOptions:function(){return b.isFunction(this.options.collectionOptions)?this.options.collectionOptions(this.instance):this.options.collectionOptions},_prepareCollection:function(a){if(this.related&&this.related.unbind("relational:add",this.handleAddition).unbind("relational:remove",this.handleRemoval).unbind("relational:reset",this.handleReset),a&&a instanceof c.Collection||(a=new this.collectionType([],this._getCollectionOptions())),a.model=this.relatedModel,this.options.collectionKey){var b=this.options.collectionKey===!0?this.options.reverseRelation.key:this.options.collectionKey;a[b]&&a[b]!==this.instance?c.Relational.showWarnings&&"undefined"!=typeof console&&console.warn("Relation=%o; collectionKey=%s already exists on collection=%o",this,b,this.options.collectionKey):b&&(a[b]=this.instance)}return a.bind("relational:add",this.handleAddition).bind("relational:remove",this.handleRemoval).bind("relational:reset",this.handleReset),a},findRelated:function(a){if(this.keyContents){var d=[];this.keyContents instanceof c.Collection?d=this.keyContents.models:(this.keyContents=b.isArray(this.keyContents)?this.keyContents:[this.keyContents],b.each(this.keyContents||[],function(a){var b=null;a instanceof this.relatedModel?b=a:(a||0===a)&&(b=this.relatedModel.findOrCreate(a,{create:this.options.createModels})),b&&!this.related.get(b)&&d.push(b)},this)),d.length&&(a=this.unsanitizeOptions(a),this.related.add(d,a))}},onChange:function(d,e,f){if(f=this.sanitizeOptions(f),this.keyContents=e,e instanceof c.Collection)this._prepareCollection(e),this.related=e;else{var g={},h={};b.isArray(e)||e===a||(e=[e]),b.each(e,function(a){h[a.id]=!0});var i=this.related;i instanceof c.Collection?b.each(i.models.slice(0),function(a){f.keepNewModels&&a.isNew()||(g[a.id]=!0,i.remove(a,{silent:a.id in h}))}):i=this._prepareCollection(),b.each(e,function(a){var b=this.relatedModel.findOrCreate(a,{create:this.options.createModels});b&&i.add(b,{silent:a.id in g})},this),this.setRelated(i)}var j=this;c.Relational.eventQueue.add(function(){!f.silentChange&&j.instance.trigger("update:"+j.key,j.instance,j.related,f)})},tryAddRelated:function(a,d){if(d=this.sanitizeOptions(d),!this.related.get(a)){var e=b.any(this.keyContents||[],function(d){var e=c.Relational.store.resolveIdForItem(this.relatedModel,d);return!b.isNull(e)&&e===a.id},this);e&&this.related.add(a,d)}},handleAddition:function(a,d,e){if(a instanceof c.Model){e=this.sanitizeOptions(e),b.each(this.getReverseRelations(a)||[],function(a){a.addRelated(this.instance,e)},this);var f=this;c.Relational.eventQueue.add(function(){!e.silentChange&&f.instance.trigger("add:"+f.key,a,f.related,e)})}},handleRemoval:function(a,d,e){if(a instanceof c.Model){e=this.sanitizeOptions(e),b.each(this.getReverseRelations(a)||[],function(a){a.removeRelated(this.instance,e)},this);var f=this;c.Relational.eventQueue.add(function(){!e.silentChange&&f.instance.trigger("remove:"+f.key,a,f.related,e)})}},handleReset:function(a,b){b=this.sanitizeOptions(b);var d=this;c.Relational.eventQueue.add(function(){!b.silentChange&&d.instance.trigger("reset:"+d.key,d.related,b)})},addRelated:function(a,b){var c=this;b=this.unsanitizeOptions(b),a.queue(function(){c.related&&!c.related.get(a)&&c.related.add(a,b)})},removeRelated:function(a,b){b=this.unsanitizeOptions(b),this.related.get(a)&&this.related.remove(a,b)}}),c.RelationalModel=c.Model.extend({relations:null,_relations:null,_isInitialized:!1,_deferProcessing:!1,_queue:null,subModelTypeAttribute:"type",subModelTypes:null,constructor:function(a,d){var e=this;if(d&&d.collection){this._deferProcessing=!0;var f=function(a){a===e&&(e._deferProcessing=!1,e.processQueue(),d.collection.unbind("relational:add",f))};d.collection.bind("relational:add",f),b.defer(function(){f(e)})}this._queue=new c.BlockingQueue,this._queue.block(),c.Relational.eventQueue.block(),c.Model.apply(this,arguments),c.Relational.eventQueue.unblock()},trigger:function(a){if(a.length>5&&"change"===a.substr(0,6)){var b=this,d=arguments;c.Relational.eventQueue.add(function(){c.Model.prototype.trigger.apply(b,d)})}else c.Model.prototype.trigger.apply(this,arguments);return this},initializeRelations:function(){this.acquire(),this._relations=[],b.each(this.relations||[],function(a){var d=b.isString(a.type)?c[a.type]||c.Relational.store.getObjectByName(a.type):a.type;d&&d.prototype instanceof c.Relation?new d(this,a):c.Relational.showWarnings&&"undefined"!=typeof console&&console.warn("Relation=%o; missing or invalid type!",a)},this),this._isInitialized=!0,this.release(),this.processQueue()},updateRelations:function(a){this._isInitialized&&!this.isLocked()&&b.each(this._relations||[],function(b){var c=this.attributes[b.keySource]||this.attributes[b.key];b.related!==c&&this.trigger("relational:change:"+b.key,this,c,a||{})},this)},queue:function(a){this._queue.add(a)},processQueue:function(){this._isInitialized&&!this._deferProcessing&&this._queue.isBlocked()&&this._queue.unblock()},getRelation:function(c){return b.detect(this._relations,function(b){return b.key===c?!0:a},this)},getRelations:function(){return this._relations},fetchRelated:function(a,d,e){d||(d={});var f,g=[],h=this.getRelation(a),i=h&&h.keyContents,j=i&&b.select(b.isArray(i)?i:[i],function(a){var d=c.Relational.store.resolveIdForItem(h.relatedModel,a);return!b.isNull(d)&&(e||!c.Relational.store.find(h.relatedModel,d))},this);if(j&&j.length){var k=b.map(j,function(a){var c;if(b.isObject(a))c=h.relatedModel.findOrCreate(a);else{var d={};d[h.relatedModel.prototype.idAttribute]=a,c=h.relatedModel.findOrCreate(d)}return c},this);if(h.related instanceof c.Collection&&b.isFunction(h.related.url)&&(f=h.related.url(k)),f&&f!==h.related.url()){var l=b.defaults({error:function(){var a=arguments;b.each(k||[],function(b){b.trigger("destroy",b,b.collection,d),d.error&&d.error.apply(b,a)})},url:f},d,{add:!0});g=[h.related.fetch(l)]}else g=b.map(k||[],function(a){var c=b.defaults({error:function(){a.trigger("destroy",a,a.collection,d),d.error&&d.error.apply(a,arguments)}},d);return a.fetch(c)},this)}return g},set:function(a,d,e){c.Relational.eventQueue.block();var f;b.isObject(a)||null==a?(f=a,e=d):(f={},f[a]=d);var g=c.Model.prototype.set.apply(this,arguments);return this._isInitialized||this.isLocked()?f&&this.idAttribute in f&&c.Relational.store.update(this):(this.constructor.initializeModelHierarchy(),c.Relational.store.register(this),this.initializeRelations()),f&&this.updateRelations(e),c.Relational.eventQueue.unblock(),g},unset:function(a,b){c.Relational.eventQueue.block();var d=c.Model.prototype.unset.apply(this,arguments);return this.updateRelations(b),c.Relational.eventQueue.unblock(),d},clear:function(a){c.Relational.eventQueue.block();var b=c.Model.prototype.clear.apply(this,arguments);return this.updateRelations(a),c.Relational.eventQueue.unblock(),b},change:function(){var b=this,d=arguments;c.Relational.eventQueue.add(function(){c.Model.prototype.change.apply(b,d)})},clone:function(){var a=b.clone(this.attributes);return b.isUndefined(a[this.idAttribute])||(a[this.idAttribute]=null),b.each(this.getRelations()||[],function(b){delete a[b.key]}),new this.constructor(a)},toJSON:function(a){if(this.isLocked())return this.id;this.acquire();var d=c.Model.prototype.toJSON.call(this,a);return!this.constructor._superModel||this.constructor._subModelTypeAttribute in d||(d[this.constructor._subModelTypeAttribute]=this.constructor._subModelTypeValue),b.each(this._relations||[],function(e){var f=d[e.key];if(e.options.includeInJSON===!0)d[e.keyDestination]=f&&b.isFunction(f.toJSON)?f.toJSON(a):null;else if(b.isString(e.options.includeInJSON))d[e.keyDestination]=f instanceof c.Collection?f.pluck(e.options.includeInJSON):f instanceof c.Model?f.get(e.options.includeInJSON):null;else if(b.isArray(e.options.includeInJSON))if(f instanceof c.Collection){var g=[];f.each(function(a){var c={};b.each(e.options.includeInJSON,function(b){c[b]=a.get(b)}),g.push(c)}),d[e.keyDestination]=g}else if(f instanceof c.Model){var g={};b.each(e.options.includeInJSON,function(a){g[a]=f.get(a)}),d[e.keyDestination]=g}else d[e.keyDestination]=null;else delete d[e.key];e.keyDestination!==e.key&&delete d[e.key]}),this.release(),d}},{setup:function(){return this.prototype.relations=(this.prototype.relations||[]).slice(0),this._subModels={},this._superModel=null,this.prototype.hasOwnProperty("subModelTypes")?c.Relational.store.addSubModels(this.prototype.subModelTypes,this):this.prototype.subModelTypes=null,b.each(this.prototype.relations||[],function(a){if(a.model||(a.model=this),a.reverseRelation&&a.model===this){var d=!0;if(b.isString(a.relatedModel)){var e=c.Relational.store.getObjectByName(a.relatedModel);d=e&&e.prototype instanceof c.RelationalModel}var f=b.isString(a.type)?c[a.type]||c.Relational.store.getObjectByName(a.type):a.type;d&&f&&f.prototype instanceof c.Relation&&new f(null,a)}},this),this},build:function(a,b){var c=this;if(this.initializeModelHierarchy(),this._subModels&&this.prototype.subModelTypeAttribute in a){var d=a[this.prototype.subModelTypeAttribute],e=this._subModels[d];e&&(c=e)}return new c(a,b)},initializeModelHierarchy:function(){if(b.isUndefined(this._superModel)||b.isNull(this._superModel))if(c.Relational.store.setupSuperModel(this),this._superModel){if(this._superModel.prototype.relations){var a=b.any(this.prototype.relations||[],function(a){return a.model&&a.model!==this},this);a||(this.prototype.relations=this._superModel.prototype.relations.concat(this.prototype.relations))}}else this._superModel=!1;this.prototype.subModelTypes&&b.keys(this.prototype.subModelTypes).length!==b.keys(this._subModels).length&&b.each(this.prototype.subModelTypes||[],function(a){var b=c.Relational.store.getObjectByName(a);b&&b.initializeModelHierarchy()})},findOrCreate:function(a,d){var e=b.isObject(a)&&this.prototype.parse?this.prototype.parse(a):a,f=c.Relational.store.find(this,e);return b.isObject(a)&&(f?f.set(e,d):(!d||d&&d.create!==!1)&&(f=this.build(a,d))),f}}),b.extend(c.RelationalModel.prototype,c.Semaphore),c.Collection.prototype.__prepareModel=c.Collection.prototype._prepareModel,c.Collection.prototype._prepareModel=function(b,d){var e;return b instanceof c.Model?(b.collection||(b.collection=this),e=b):(d||(d={}),d.collection=this,e=this.model.findOrCreate!==a?this.model.findOrCreate(b,d):new this.model(b,d),e._validate(b,d)||(e=!1)),e};var e=c.Collection.prototype.__add=c.Collection.prototype.add;c.Collection.prototype.add=function(a,d){d||(d={}),b.isArray(a)||(a=[a]);var f=[];return b.each(a||[],function(a){a instanceof c.Model||(a=c.Collection.prototype._prepareModel.call(this,a,d)),a instanceof c.Model&&!this.get(a)&&f.push(a)},this),f.length&&(e.call(this,f,d),b.each(f||[],function(a){this.trigger("relational:add",a,this,d)},this)),this};var f=c.Collection.prototype.__remove=c.Collection.prototype.remove;c.Collection.prototype.remove=function(a,d){return d||(d={}),a=b.isArray(a)?a.slice(0):[a],b.each(a||[],function(a){a=this.get(a),a instanceof c.Model&&(f.call(this,a,d),this.trigger("relational:remove",a,this,d))},this),this};var g=c.Collection.prototype.__reset=c.Collection.prototype.reset;c.Collection.prototype.reset=function(a,b){return g.call(this,a,b),this.trigger("relational:reset",this,b),this};var h=c.Collection.prototype.__sort=c.Collection.prototype.sort;c.Collection.prototype.sort=function(a){return h.call(this,a),this.trigger("relational:reset",this,a),this};var i=c.Collection.prototype.__trigger=c.Collection.prototype.trigger;c.Collection.prototype.trigger=function(a){if("add"===a||"remove"===a||"reset"===a){var d=this,e=arguments;"add"===a&&(e=b.toArray(e),b.isObject(e[3])&&(e[3]=b.clone(e[3]))),c.Relational.eventQueue.add(function(){i.apply(d,e)})}else i.apply(this,arguments);return this},c.RelationalModel.extend=function(){var d=c.Model.extend.apply(this,arguments);return d.setup(this),d}})();
(function(undefined){"use strict";var _,Backbone,exports;if(typeof window==="undefined"){_=require("underscore");Backbone=require("backbone");exports=module.exports=Backbone}else{_=window._;Backbone=window.Backbone;exports=window}Backbone.Relational={showWarnings:true};Backbone.Semaphore={_permitsAvailable:null,_permitsUsed:0,acquire:function(){if(this._permitsAvailable&&this._permitsUsed>=this._permitsAvailable){throw new Error("Max permits acquired")}else{this._permitsUsed++}},release:function(){if(this._permitsUsed===0){throw new Error("All permits released")}else{this._permitsUsed--}},isLocked:function(){return this._permitsUsed>0},setAvailablePermits:function(amount){if(this._permitsUsed>amount){throw new Error("Available permits cannot be less than used permits")}this._permitsAvailable=amount}};Backbone.BlockingQueue=function(){this._queue=[]};_.extend(Backbone.BlockingQueue.prototype,Backbone.Semaphore,{_queue:null,add:function(func){if(this.isBlocked()){this._queue.push(func)}else{func()}},process:function(){while(this._queue&&this._queue.length){this._queue.shift()()}},block:function(){this.acquire()},unblock:function(){this.release();if(!this.isBlocked()){this.process()}},isBlocked:function(){return this.isLocked()}});Backbone.Relational.eventQueue=new Backbone.BlockingQueue;Backbone.Store=function(){this._collections=[];this._reverseRelations=[];this._orphanRelations=[];this._subModels=[];this._modelScopes=[exports]};_.extend(Backbone.Store.prototype,Backbone.Events,{initializeRelation:function(model,relation,options){var type=!_.isString(relation.type)?relation.type:Backbone[relation.type]||this.getObjectByName(relation.type);if(type&&type.prototype instanceof Backbone.Relation){new type(model,relation,options)}else{Backbone.Relational.showWarnings&&typeof console!=="undefined"&&console.warn("Relation=%o; missing or invalid relation type!",relation)}},addModelScope:function(scope){this._modelScopes.push(scope)},addSubModels:function(subModelTypes,superModelType){this._subModels.push({superModelType:superModelType,subModels:subModelTypes})},setupSuperModel:function(modelType){_.find(this._subModels,function(subModelDef){return _.find(subModelDef.subModels||[],function(subModelTypeName,typeValue){var subModelType=this.getObjectByName(subModelTypeName);if(modelType===subModelType){subModelDef.superModelType._subModels[typeValue]=modelType;modelType._superModel=subModelDef.superModelType;modelType._subModelTypeValue=typeValue;modelType._subModelTypeAttribute=subModelDef.superModelType.prototype.subModelTypeAttribute;return true}},this)},this)},addReverseRelation:function(relation){var exists=_.any(this._reverseRelations,function(rel){return _.all(relation||[],function(val,key){return val===rel[key]})});if(!exists&&relation.model&&relation.type){this._reverseRelations.push(relation);this._addRelation(relation.model,relation);this.retroFitRelation(relation)}},addOrphanRelation:function(relation){var exists=_.any(this._orphanRelations,function(rel){return _.all(relation||[],function(val,key){return val===rel[key]})});if(!exists&&relation.model&&relation.type){this._orphanRelations.push(relation)}},processOrphanRelations:function(){_.each(this._orphanRelations.slice(0),function(rel){var relatedModel=Backbone.Relational.store.getObjectByName(rel.relatedModel);if(relatedModel){this.initializeRelation(null,rel);this._orphanRelations=_.without(this._orphanRelations,rel)}},this)},_addRelation:function(type,relation){if(!type.prototype.relations){type.prototype.relations=[]}type.prototype.relations.push(relation);_.each(type._subModels||[],function(subModel){this._addRelation(subModel,relation)},this)},retroFitRelation:function(relation){var coll=this.getCollection(relation.model,false);coll&&coll.each(function(model){if(!(model instanceof relation.model)){return}new relation.type(model,relation)},this)},getCollection:function(type,create){if(type instanceof Backbone.RelationalModel){type=type.constructor}var rootModel=type;while(rootModel._superModel){rootModel=rootModel._superModel}var coll=_.findWhere(this._collections,{model:rootModel});if(!coll&&create!==false){coll=this._createCollection(rootModel)}return coll},getObjectByName:function(name){var parts=name.split("."),type=null;_.find(this._modelScopes,function(scope){type=_.reduce(parts||[],function(memo,val){return memo?memo[val]:undefined},scope);if(type&&type!==scope){return true}},this);return type},_createCollection:function(type){var coll;if(type instanceof Backbone.RelationalModel){type=type.constructor}if(type.prototype instanceof Backbone.RelationalModel){coll=new Backbone.Collection;coll.model=type;this._collections.push(coll)}return coll},resolveIdForItem:function(type,item){var id=_.isString(item)||_.isNumber(item)?item:null;if(id===null){if(item instanceof Backbone.RelationalModel){id=item.id}else if(_.isObject(item)){id=item[type.prototype.idAttribute]}}if(!id&&id!==0){id=null}return id},find:function(type,item){var id=this.resolveIdForItem(type,item);var coll=this.getCollection(type);if(coll){var obj=coll.get(id);if(obj instanceof type){return obj}}return null},register:function(model){var coll=this.getCollection(model);if(coll){if(coll.get(model)){if(Backbone.Relational.showWarnings&&typeof console!=="undefined"){console.warn("Duplicate id! Old RelationalModel=%o, new RelationalModel=%o",coll.get(model),model)}throw new Error("Cannot instantiate more than one Backbone.RelationalModel with the same id per type!")}var modelColl=model.collection;coll.add(model);this.listenTo(model,"destroy",this.unregister,this);model.collection=modelColl}},update:function(model){var coll=this.getCollection(model);coll._onModelEvent("change:"+model.idAttribute,model,coll)},unregister:function(model){this.stopListening(model,"destroy",this.unregister);var coll=this.getCollection(model);coll&&coll.remove(model)},reset:function(){this.stopListening();this._collections=[];this._subModels=[];this._modelScopes=[exports]}});Backbone.Relational.store=new Backbone.Store;Backbone.Relation=function(instance,options,opts){this.instance=instance;options=_.isObject(options)?options:{};this.reverseRelation=_.defaults(options.reverseRelation||{},this.options.reverseRelation);this.options=_.defaults(options,this.options,Backbone.Relation.prototype.options);this.reverseRelation.type=!_.isString(this.reverseRelation.type)?this.reverseRelation.type:Backbone[this.reverseRelation.type]||Backbone.Relational.store.getObjectByName(this.reverseRelation.type);this.key=this.options.key;this.keySource=this.options.keySource||this.key;this.keyDestination=this.options.keyDestination||this.keySource||this.key;this.model=this.options.model||this.instance.constructor;this.relatedModel=this.options.relatedModel;if(_.isString(this.relatedModel)){this.relatedModel=Backbone.Relational.store.getObjectByName(this.relatedModel)}if(!this.checkPreconditions()){return}if(!this.options.isAutoRelation&&this.reverseRelation.type&&this.reverseRelation.key){Backbone.Relational.store.addReverseRelation(_.defaults({isAutoRelation:true,model:this.relatedModel,relatedModel:this.model,reverseRelation:this.options},this.reverseRelation))}if(instance){var contentKey=this.keySource;if(contentKey!==this.key&&typeof this.instance.get(this.key)==="object"){contentKey=this.key}this.setKeyContents(this.instance.get(contentKey));this.relatedCollection=Backbone.Relational.store.getCollection(this.relatedModel);if(this.keySource!==this.key){this.instance.unset(this.keySource,{silent:true})}this.instance._relations[this.key]=this;this.initialize(opts);if(this.options.autoFetch){this.instance.fetchRelated(this.key,_.isObject(this.options.autoFetch)?this.options.autoFetch:{})}this.listenTo(this.instance,"destroy",this.destroy).listenTo(this.relatedCollection,"relational:add",this.tryAddRelated).listenTo(this.relatedCollection,"relational:remove",this.removeRelated)}};Backbone.Relation.extend=Backbone.Model.extend;_.extend(Backbone.Relation.prototype,Backbone.Events,Backbone.Semaphore,{options:{createModels:true,includeInJSON:true,isAutoRelation:false,autoFetch:false,parse:false},instance:null,key:null,keyContents:null,relatedModel:null,relatedCollection:null,reverseRelation:null,related:null,checkPreconditions:function(){var i=this.instance,k=this.key,m=this.model,rm=this.relatedModel,warn=Backbone.Relational.showWarnings&&typeof console!=="undefined";if(!m||!k||!rm){warn&&console.warn("Relation=%o: missing model, key or relatedModel (%o, %o, %o).",this,m,k,rm);return false}if(!(m.prototype instanceof Backbone.RelationalModel)){warn&&console.warn("Relation=%o: model does not inherit from Backbone.RelationalModel (%o).",this,i);return false}if(!(rm.prototype instanceof Backbone.RelationalModel)){warn&&console.warn("Relation=%o: relatedModel does not inherit from Backbone.RelationalModel (%o).",this,rm);return false}if(this instanceof Backbone.HasMany&&this.reverseRelation.type===Backbone.HasMany){warn&&console.warn("Relation=%o: relation is a HasMany, and the reverseRelation is HasMany as well.",this);return false}if(i&&_.keys(i._relations).length){var existing=_.find(i._relations,function(rel){return rel.key===k},this);if(existing){warn&&console.warn("Cannot create relation=%o on %o for model=%o: already taken by relation=%o.",this,k,i,existing);return false}}return true},setRelated:function(related){this.related=related;this.instance.acquire();this.instance.attributes[this.key]=related;this.instance.release()},_isReverseRelation:function(relation){return relation.instance instanceof this.relatedModel&&this.reverseRelation.key===relation.key&&this.key===relation.reverseRelation.key},getReverseRelations:function(model){var reverseRelations=[];var models=!_.isUndefined(model)?[model]:this.related&&(this.related.models||[this.related]);_.each(models||[],function(related){_.each(related.getRelations()||[],function(relation){if(this._isReverseRelation(relation)){reverseRelations.push(relation)}},this)},this);return reverseRelations},destroy:function(){this.stopListening();if(this instanceof Backbone.HasOne){this.setRelated(null)}else if(this instanceof Backbone.HasMany){this.setRelated(this._prepareCollection())}_.each(this.getReverseRelations(),function(relation){relation.removeRelated(this.instance)},this)}});Backbone.HasOne=Backbone.Relation.extend({options:{reverseRelation:{type:"HasMany"}},initialize:function(opts){this.listenTo(this.instance,"relational:change:"+this.key,this.onChange);var related=this.findRelated(opts);this.setRelated(related);_.each(this.getReverseRelations(),function(relation){relation.addRelated(this.instance,opts)},this)},findRelated:function(options){var related=null;options=_.defaults({parse:this.options.parse},options);if(this.keyContents instanceof this.relatedModel){related=this.keyContents}else if(this.keyContents||this.keyContents===0){var opts=_.defaults({create:this.options.createModels},options);related=this.relatedModel.findOrCreate(this.keyContents,opts)}return related},setKeyContents:function(keyContents){this.keyContents=keyContents;this.keyId=Backbone.Relational.store.resolveIdForItem(this.relatedModel,this.keyContents)},onChange:function(model,attr,options){if(this.isLocked()){return}this.acquire();options=options?_.clone(options):{};var changed=_.isUndefined(options.__related),oldRelated=changed?this.related:options.__related;if(changed){this.setKeyContents(attr);var related=this.findRelated(options);this.setRelated(related)}if(oldRelated&&this.related!==oldRelated){_.each(this.getReverseRelations(oldRelated),function(relation){relation.removeRelated(this.instance,null,options)},this)}_.each(this.getReverseRelations(),function(relation){relation.addRelated(this.instance,options)},this);if(!options.silent&&this.related!==oldRelated){var dit=this;this.changed=true;Backbone.Relational.eventQueue.add(function(){dit.instance.trigger("change:"+dit.key,dit.instance,dit.related,options,true);dit.changed=false})}this.release()},tryAddRelated:function(model,coll,options){if((this.keyId||this.keyId===0)&&model.id===this.keyId){this.addRelated(model,options);this.keyId=null}},addRelated:function(model,options){var dit=this;model.queue(function(){if(model!==dit.related){var oldRelated=dit.related||null;dit.setRelated(model);dit.onChange(dit.instance,model,_.defaults({__related:oldRelated},options))}})},removeRelated:function(model,coll,options){if(!this.related){return}if(model===this.related){var oldRelated=this.related||null;this.setRelated(null);this.onChange(this.instance,model,_.defaults({__related:oldRelated},options))}}});Backbone.HasMany=Backbone.Relation.extend({collectionType:null,options:{reverseRelation:{type:"HasOne"},collectionType:Backbone.Collection,collectionKey:true,collectionOptions:{}},initialize:function(opts){this.listenTo(this.instance,"relational:change:"+this.key,this.onChange);this.collectionType=this.options.collectionType;if(_.isString(this.collectionType)){this.collectionType=Backbone.Relational.store.getObjectByName(this.collectionType)}if(!this.collectionType.prototype instanceof Backbone.Collection){throw new Error("`collectionType` must inherit from Backbone.Collection")}var related=this.findRelated(opts);this.setRelated(related)},_prepareCollection:function(collection){if(this.related){this.stopListening(this.related)}if(!collection||!(collection instanceof Backbone.Collection)){var options=_.isFunction(this.options.collectionOptions)?this.options.collectionOptions(this.instance):this.options.collectionOptions;collection=new this.collectionType(null,options)}collection.model=this.relatedModel;if(this.options.collectionKey){var key=this.options.collectionKey===true?this.options.reverseRelation.key:this.options.collectionKey;if(collection[key]&&collection[key]!==this.instance){if(Backbone.Relational.showWarnings&&typeof console!=="undefined"){console.warn("Relation=%o; collectionKey=%s already exists on collection=%o",this,key,this.options.collectionKey)}}else if(key){collection[key]=this.instance}}this.listenTo(collection,"relational:add",this.handleAddition).listenTo(collection,"relational:remove",this.handleRemoval).listenTo(collection,"relational:reset",this.handleReset);return collection},findRelated:function(options){var related=null;options=_.defaults({parse:this.options.parse},options);if(this.keyContents instanceof Backbone.Collection){this._prepareCollection(this.keyContents);related=this.keyContents}else{var toAdd=[];_.each(this.keyContents,function(attributes){if(attributes instanceof this.relatedModel){var model=attributes}else{model=this.relatedModel.findOrCreate(attributes,_.extend({merge:true},options,{create:this.options.createModels}))}model&&toAdd.push(model)},this);if(this.related instanceof Backbone.Collection){related=this.related}else{related=this._prepareCollection()}related.update(toAdd,_.defaults({merge:false,parse:false},options))}return related},setKeyContents:function(keyContents){this.keyContents=keyContents instanceof Backbone.Collection?keyContents:null;this.keyIds=[];if(!this.keyContents&&(keyContents||keyContents===0)){this.keyContents=_.isArray(keyContents)?keyContents:[keyContents];_.each(this.keyContents,function(item){var itemId=Backbone.Relational.store.resolveIdForItem(this.relatedModel,item);if(itemId||itemId===0){this.keyIds.push(itemId)}},this)}},onChange:function(model,attr,options){options=options?_.clone(options):{};this.setKeyContents(attr);this.changed=false;var related=this.findRelated(options);this.setRelated(related);if(!options.silent){var dit=this;Backbone.Relational.eventQueue.add(function(){if(dit.changed){dit.instance.trigger("change:"+dit.key,dit.instance,dit.related,options,true);dit.changed=false}})}},handleAddition:function(model,coll,options){options=options?_.clone(options):{};this.changed=true;_.each(this.getReverseRelations(model),function(relation){relation.addRelated(this.instance,options)},this);var dit=this;!options.silent&&Backbone.Relational.eventQueue.add(function(){dit.instance.trigger("add:"+dit.key,model,dit.related,options)})},handleRemoval:function(model,coll,options){options=options?_.clone(options):{};this.changed=true;_.each(this.getReverseRelations(model),function(relation){relation.removeRelated(this.instance,null,options)},this);var dit=this;!options.silent&&Backbone.Relational.eventQueue.add(function(){dit.instance.trigger("remove:"+dit.key,model,dit.related,options)})},handleReset:function(coll,options){var dit=this;options=options?_.clone(options):{};!options.silent&&Backbone.Relational.eventQueue.add(function(){dit.instance.trigger("reset:"+dit.key,dit.related,options)})},tryAddRelated:function(model,coll,options){var item=_.contains(this.keyIds,model.id);if(item){this.addRelated(model,options);this.keyIds=_.without(this.keyIds,model.id)}},addRelated:function(model,options){var dit=this;model.queue(function(){if(dit.related&&!dit.related.get(model)){dit.related.add(model,options)}})},removeRelated:function(model,coll,options){if(this.related.get(model)){this.related.remove(model,options)}}});Backbone.RelationalModel=Backbone.Model.extend({relations:null,_relations:null,_isInitialized:false,_deferProcessing:false,_queue:null,subModelTypeAttribute:"type",subModelTypes:null,constructor:function(attributes,options){if(options&&options.collection){var dit=this,collection=this.collection=options.collection;delete options.collection;this._deferProcessing=true;var processQueue=function(model){if(model===dit){dit._deferProcessing=false;dit.processQueue();collection.off("relational:add",processQueue)}};collection.on("relational:add",processQueue);_.defer(function(){processQueue(dit)})}Backbone.Relational.store.processOrphanRelations();this._queue=new Backbone.BlockingQueue;this._queue.block();Backbone.Relational.eventQueue.block();try{Backbone.Model.apply(this,arguments)}finally{Backbone.Relational.eventQueue.unblock()}},trigger:function(eventName){if(eventName.length>5&&eventName.indexOf("change")===0){var dit=this,args=arguments;Backbone.Relational.eventQueue.add(function(){if(!dit._isInitialized){return}var changed=true;if(eventName==="change"){changed=dit.hasChanged()}else{var attr=eventName.slice(7),rel=dit.getRelation(attr);if(rel){changed=args[4]===true;if(changed){dit.changed[attr]=args[2]}else if(!rel.changed){delete dit.changed[attr]}}}changed&&Backbone.Model.prototype.trigger.apply(dit,args)})}else{Backbone.Model.prototype.trigger.apply(this,arguments)}return this},initializeRelations:function(options){this.acquire();this._relations={};_.each(this.relations||[],function(rel){Backbone.Relational.store.initializeRelation(this,rel,options)},this);this._isInitialized=true;this.release();this.processQueue()},updateRelations:function(options){if(this._isInitialized&&!this.isLocked()){_.each(this._relations,function(rel){var val=this.attributes[rel.keySource]||this.attributes[rel.key];if(rel.related!==val){this.trigger("relational:change:"+rel.key,this,val,options||{})}},this)}},queue:function(func){this._queue.add(func)},processQueue:function(){if(this._isInitialized&&!this._deferProcessing&&this._queue.isBlocked()){this._queue.unblock()}},getRelation:function(key){return this._relations[key]},getRelations:function(){return _.values(this._relations)},fetchRelated:function(key,options,refresh){options=_.extend({update:true,remove:false},options);var setUrl,requests=[],rel=this.getRelation(key),keys=rel&&(rel.keyIds||[rel.keyId]),toFetch=keys&&_.select(keys||[],function(id){return(id||id===0)&&(refresh||!Backbone.Relational.store.find(rel.relatedModel,id))},this);if(toFetch&&toFetch.length){var created=[],models=_.map(toFetch,function(id){var model=Backbone.Relational.store.find(rel.relatedModel,id);if(!model){var attrs={};attrs[rel.relatedModel.prototype.idAttribute]=id;model=rel.relatedModel.findOrCreate(attrs,options);created.push(model)}return model},this);if(rel.related instanceof Backbone.Collection&&_.isFunction(rel.related.url)){setUrl=rel.related.url(models)}if(setUrl&&setUrl!==rel.related.url()){var opts=_.defaults({error:function(){var args=arguments;_.each(created,function(model){model.trigger("destroy",model,model.collection,options);options.error&&options.error.apply(model,args)})},url:setUrl},options);requests=[rel.related.fetch(opts)]}else{requests=_.map(models,function(model){var opts=_.defaults({error:function(){if(_.contains(created,model)){model.trigger("destroy",model,model.collection,options);options.error&&options.error.apply(model,arguments)}}},options);return model.fetch(opts)},this)}}return requests},get:function(attr){var originalResult=Backbone.Model.prototype.get.call(this,attr);if(!this.dotNotation||attr.indexOf(".")===-1){return originalResult}var splits=attr.split(".");var result=_.reduce(splits,function(model,split){if(!(model instanceof Backbone.Model)){throw new Error("Attribute must be an instanceof Backbone.Model. Is: "+model+", currentSplit: "+split)}return Backbone.Model.prototype.get.call(model,split)},this);if(originalResult!==undefined&&result!==undefined){throw new Error("Ambiguous result for '"+attr+"'. direct result: "+originalResult+", dotNotation: "+result)}return originalResult||result},set:function(key,value,options){Backbone.Relational.eventQueue.block();var attributes;if(_.isObject(key)||key==null){attributes=key;options=value}else{attributes={};attributes[key]=value}var result=Backbone.Model.prototype.set.apply(this,arguments);try{if(!this._isInitialized&&!this.isLocked()){this.constructor.initializeModelHierarchy();Backbone.Relational.store.register(this);this.initializeRelations(options)}else if(attributes&&this.idAttribute in attributes){Backbone.Relational.store.update(this)}if(attributes){this.updateRelations(options)}}finally{Backbone.Relational.eventQueue.unblock()}return result},unset:function(attribute,options){Backbone.Relational.eventQueue.block();var result=Backbone.Model.prototype.unset.apply(this,arguments);this.updateRelations(options);Backbone.Relational.eventQueue.unblock();return result},clear:function(options){Backbone.Relational.eventQueue.block();var result=Backbone.Model.prototype.clear.apply(this,arguments);this.updateRelations(options);Backbone.Relational.eventQueue.unblock();return result},clone:function(){var attributes=_.clone(this.attributes);if(!_.isUndefined(attributes[this.idAttribute])){attributes[this.idAttribute]=null}_.each(this.getRelations(),function(rel){delete attributes[rel.key]});return new this.constructor(attributes)},toJSON:function(options){if(this.isLocked()){return this.id}this.acquire();var json=Backbone.Model.prototype.toJSON.call(this,options);if(this.constructor._superModel&&!(this.constructor._subModelTypeAttribute in json)){json[this.constructor._subModelTypeAttribute]=this.constructor._subModelTypeValue}_.each(this._relations,function(rel){var value=json[rel.key];if(rel.options.includeInJSON===true){if(value&&_.isFunction(value.toJSON)){json[rel.keyDestination]=value.toJSON(options)}else{json[rel.keyDestination]=null}}else if(_.isString(rel.options.includeInJSON)){if(value instanceof Backbone.Collection){json[rel.keyDestination]=value.pluck(rel.options.includeInJSON)}else if(value instanceof Backbone.Model){json[rel.keyDestination]=value.get(rel.options.includeInJSON)}else{json[rel.keyDestination]=null}}else if(_.isArray(rel.options.includeInJSON)){if(value instanceof Backbone.Collection){var valueSub=[];value.each(function(model){var curJson={};_.each(rel.options.includeInJSON,function(key){curJson[key]=model.get(key)});valueSub.push(curJson)});json[rel.keyDestination]=valueSub}else if(value instanceof Backbone.Model){var valueSub={};_.each(rel.options.includeInJSON,function(key){valueSub[key]=value.get(key)});json[rel.keyDestination]=valueSub}else{json[rel.keyDestination]=null}}else{delete json[rel.key]}if(rel.keyDestination!==rel.key){delete json[rel.key]}});this.release();return json}},{setup:function(superModel){this.prototype.relations=(this.prototype.relations||[]).slice(0);this._subModels={};this._superModel=null;if(this.prototype.hasOwnProperty("subModelTypes")){Backbone.Relational.store.addSubModels(this.prototype.subModelTypes,this)}else{this.prototype.subModelTypes=null}_.each(this.prototype.relations||[],function(rel){if(!rel.model){rel.model=this}if(rel.reverseRelation&&rel.model===this){var preInitialize=true;if(_.isString(rel.relatedModel)){var relatedModel=Backbone.Relational.store.getObjectByName(rel.relatedModel);preInitialize=relatedModel&&relatedModel.prototype instanceof Backbone.RelationalModel}if(preInitialize){Backbone.Relational.store.initializeRelation(null,rel)}else if(_.isString(rel.relatedModel)){Backbone.Relational.store.addOrphanRelation(rel)}}},this);return this},build:function(attributes,options){var model=this;this.initializeModelHierarchy();if(this._subModels&&this.prototype.subModelTypeAttribute in attributes){var subModelTypeAttribute=attributes[this.prototype.subModelTypeAttribute];var subModelType=this._subModels[subModelTypeAttribute];if(subModelType){model=subModelType}}return new model(attributes,options)},initializeModelHierarchy:function(){if(_.isUndefined(this._superModel)||_.isNull(this._superModel)){Backbone.Relational.store.setupSuperModel(this);if(this._superModel){if(this._superModel.prototype.relations){var supermodelRelationsExist=_.any(this.prototype.relations||[],function(rel){return rel.model&&rel.model!==this},this);if(!supermodelRelationsExist){this.prototype.relations=this._superModel.prototype.relations.concat(this.prototype.relations)}}}else{this._superModel=false}}if(this.prototype.subModelTypes&&_.keys(this.prototype.subModelTypes).length!==_.keys(this._subModels).length){_.each(this.prototype.subModelTypes||[],function(subModelTypeName){var subModelType=Backbone.Relational.store.getObjectByName(subModelTypeName);subModelType&&subModelType.initializeModelHierarchy()})}},findOrCreate:function(attributes,options){options||(options={});var parsedAttributes=_.isObject(attributes)&&options.parse&&this.prototype.parse?this.prototype.parse(attributes):attributes;var model=Backbone.Relational.store.find(this,parsedAttributes);if(_.isObject(attributes)){if(model&&options.merge!==false){model.set(parsedAttributes,options)}else if(!model&&options.create!==false){model=this.build(attributes,options)}}return model}});_.extend(Backbone.RelationalModel.prototype,Backbone.Semaphore);Backbone.Collection.prototype.__prepareModel=Backbone.Collection.prototype._prepareModel;Backbone.Collection.prototype._prepareModel=function(attrs,options){var model;if(attrs instanceof Backbone.Model){if(!attrs.collection){attrs.collection=this}model=attrs}else{options||(options={});options.collection=this;if(typeof this.model.findOrCreate!=="undefined"){model=this.model.findOrCreate(attrs,options)}else{model=new this.model(attrs,options)}if(model&&model.isNew()&&!model._validate(attrs,options)){this.trigger("invalid",this,attrs,options);model=false}}return model};var add=Backbone.Collection.prototype.__add=Backbone.Collection.prototype.add;Backbone.Collection.prototype.add=function(models,options){if(!(this.model.prototype instanceof Backbone.RelationalModel)){return add.apply(this,arguments)}models=_.isArray(models)?models.slice():[models];options=_.extend({merge:false},options);var newModels=[],toAdd=[];_.each(models,function(model){if(!(model instanceof Backbone.Model)){model=Backbone.Collection.prototype._prepareModel.call(this,model,options)}if(model){toAdd.push(model);if(!(this.get(model)||this.get(model.cid))){newModels.push(model)}else if(model.id!=null){this._byId[model.id]=model}}},this);add.call(this,toAdd,options);_.each(newModels,function(model){if(this.get(model)||this.get(model.cid)){this.trigger("relational:add",model,this,options)}},this);return this};var remove=Backbone.Collection.prototype.__remove=Backbone.Collection.prototype.remove;Backbone.Collection.prototype.remove=function(models,options){if(!(this.model.prototype instanceof Backbone.RelationalModel)){return remove.apply(this,arguments)}models=_.isArray(models)?models.slice():[models];options||(options={});var toRemove=[];_.each(models,function(model){model=this.get(model)||this.get(model.cid);model&&toRemove.push(model)},this);if(toRemove.length){remove.call(this,toRemove,options);_.each(toRemove,function(model){this.trigger("relational:remove",model,this,options)},this)}return this};var reset=Backbone.Collection.prototype.__reset=Backbone.Collection.prototype.reset;Backbone.Collection.prototype.reset=function(models,options){reset.call(this,models,options);if(this.model.prototype instanceof Backbone.RelationalModel){this.trigger("relational:reset",this,options)}return this};var sort=Backbone.Collection.prototype.__sort=Backbone.Collection.prototype.sort;Backbone.Collection.prototype.sort=function(options){sort.call(this,options);if(this.model.prototype instanceof Backbone.RelationalModel){this.trigger("relational:reset",this,options)}return this};var trigger=Backbone.Collection.prototype.__trigger=Backbone.Collection.prototype.trigger;Backbone.Collection.prototype.trigger=function(eventName){if(!(this.model.prototype instanceof Backbone.RelationalModel)){return trigger.apply(this,arguments)}if(eventName==="add"||eventName==="remove"||eventName==="reset"){var dit=this,args=arguments;if(_.isObject(args[3])){args=_.toArray(args);args[3]=_.clone(args[3])}Backbone.Relational.eventQueue.add(function(){trigger.apply(dit,args)})}else{trigger.apply(this,arguments)}return this};Backbone.RelationalModel.extend=function(protoProps,classProps){var child=Backbone.Model.extend.apply(this,arguments);child.setup(this);return child}})();

@@ -16,4 +16,4 @@ {

"optionalDependencies": {
"underscore": ">=1.4.3",
"backbone": ">=0.9.9"
"underscore": ">=1.4.4",
"backbone": ">=0.9.10"
},

@@ -23,3 +23,3 @@

"main" : "backbone-relational.js",
"version" : "0.7.0"
"version" : "0.8.0"
}
SocketSocket SOC 2 Logo

Product

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

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc