Last active
August 29, 2015 14:15
-
-
Save malte-wessel/872dc16c1ffb367af49c to your computer and use it in GitHub Desktop.
saveAll, merge extension for Bookshelf
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var _ = require('lodash'), | |
Promise = require('bluebird'); | |
// No, this is not nice, but there's no other way to get the helpers | |
var Helpers = require('node_modules/bookshelf/lib/helpers'); | |
var database = require('app/core/database'); | |
var Collection = database.Collection.extend({ | |
/** | |
* Updates or creates the collection's models | |
* All new models (models that don't have an id) will be created with a single insert statement | |
* Existing models (models that do have an id) will be updated by invoking save() on each single model. | |
* @param {options} options | |
* @return {Promise} | |
*/ | |
saveAll: function(options) { | |
var promises = [], | |
existingModels = [], | |
newModels = []; | |
// Split models into existing and new models | |
this.each(function(model) { | |
if(model.isNew()) return newModels.push(model); | |
return existingModels.push(model); | |
}); | |
// Update existing rows | |
_.each(existingModels, function(model) { | |
if(!model.hasChanged()) return; | |
// Get the changed attributes | |
var changed = _.filter(model.keys(), function(attr) { | |
return model.hasChanged(attr); | |
}); | |
// If nothing has changed, we are done! | |
if(changed.length < 1) return; | |
// Save only the changed fields (patch) | |
var save = model.save(model.pick(changed), _.extend({patch: true}, options)); | |
promises.push(save); | |
}, this); | |
// No new models? Then we'll skip this step. | |
if(newModels.length > 0) { | |
// Map models to ready-to-insert model data, | |
// Sets defaults, timestamp and save constrainst | |
// for each model | |
insertData = _.map(newModels, function(model){ | |
var attributes = model.attributes; | |
// If the model has timestamp columns, | |
// set them as attributes on the model | |
if (model.hasTimestamps) _.extend(attributes, model.timestamp(options)); | |
// Merge any defaults here | |
var defaults = _.result(model, 'defaults'); | |
if (defaults) attributes = _.extend({}, defaults, attributes); | |
// Update the models with the modified attributes | |
model.set(attributes, options); | |
// Save contstraints | |
Helpers.saveConstraints(model, this.relatedData); | |
// Return the ready to insert model data | |
return model.format(model.attributes); | |
}, this); | |
// Performs the database insert. | |
// After the rows have been saved, the models | |
// will be updated with their new ids | |
var insert = this | |
.query() | |
.insert(insertData, this.model.idAttribute) | |
.bind(this) | |
.then(function(ids) { | |
// Mysql returns the first id of the created models, Postgres returns all ids | |
// http://knexjs.org/#Builder-insert | |
// http://stackoverflow.com/a/1285278/1818705 | |
if (ids.length === 1 && ids.length < newModels.length) { | |
ids = _.range(ids[0], ids[0] + newModels.length); | |
} | |
_.each(newModels, function(model) { | |
// Update the new models with the received ids | |
model.set(model.idAttribute, ids.shift(), options); | |
// @TODO test triggering events | |
// Trigger created, saved events | |
model.trigger('created saved', model, null, options); | |
}); | |
return this; | |
}); | |
// Push the insert promise | |
promises.push(insert); | |
} | |
// Return promise | |
return Promise.all(promises).return(this); | |
}, | |
/** | |
* Merge data into the collection | |
* Since new models do not have an id, you can specify an attribute to identify existing models. | |
* Existing models (present in the collection) and new models (passed data), that share the same value | |
* of the identifier attribute will be merged. The identifier attribute can be specified in the | |
* options hash like options.identifier = "external_id" | |
* If you only want to update certain fields, you can specify a fields attribute in the options hash. | |
* @param {Array} data Array of new models | |
* @param {Object} options | |
* @return {Collection} | |
*/ | |
merge: function(data, options) { | |
options = options || {}; | |
var idAttribute = this.idAttribute(), | |
identifier = options.identifier || idAttribute, | |
fields = options.fields; | |
// Index existing models by identifier | |
var modelsByIdentifier = _.indexBy(this.models, function(model) { | |
return model.get(identifier); | |
}); | |
// Map data for Collection.set | |
data = _.map(data, function(model) { | |
var existing = modelsByIdentifier[model[identifier]]; | |
// No model found with the same identifier field, | |
// return the new model | |
if(!existing) return model; | |
// Existing model found, grab id and return the model | |
model = fields ? _.pick(model, fields) : model; | |
existing = existing.pick(idAttribute); | |
return _.extend(existing, model); | |
}); | |
return this.set(data, options); | |
} | |
}); | |
module.exports = database.collection('Collection', Collection); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment