Skip to content

Instantly share code, notes, and snippets.

@malte-wessel
Last active August 29, 2015 14:15
Show Gist options
  • Save malte-wessel/872dc16c1ffb367af49c to your computer and use it in GitHub Desktop.
Save malte-wessel/872dc16c1ffb367af49c to your computer and use it in GitHub Desktop.
saveAll, merge extension for Bookshelf
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