Skip to content

Instantly share code, notes, and snippets.

@optilude
Created April 22, 2014 12:10
Show Gist options
  • Save optilude/11176381 to your computer and use it in GitHub Desktop.
Save optilude/11176381 to your computer and use it in GitHub Desktop.
syncModels: function(model, collection, where, include, keep) {
var collectionItems = {}, // lookup: id -> item from client
toUpdate = {}, // lookup: id -> db model
toCreate = [], // models
toDelete = []; // models
include = include || [];
// keep track of models we are updating
collection.forEach(function(item) {
if(item.id) {
collectionItems[item.id] = item;
}
});
// helper function to recursively sync children
function syncRecursive(obj, item) {
if(!obj.Model.associations)
return null;
return Promise.all(_.map(obj.Model.associations, function(association) {
// XXX: Other types not yet implemented
if(association.associationType != "HasMany") {
return null;
}
var target = association.target,
key = normalizeCase(association.options.as? association.options.as : target.tableName),
val = item[key],
where = {};
if(val === undefined) {
return null;
}
where[association.identifier] = obj.id;
return module.exports.utils.syncModels(target, val, where, [], keep);
}));
}
return model.findAll({
where: where,
include: include
})
.then(function(response) {
// Keep track of which models we'll need to create, update and delete
response.forEach(function(item) {
var matchingModel = collectionItems[item.id];
if(matchingModel === undefined) {
if(!keep) {
toDelete.push(item);
}
} else {
toUpdate[item.id] = item;
}
});
toCreate = collection.filter(function(item) {
return !item.id || toUpdate[item.id] === undefined;
});
// Execute database operations
return Promise.join(
// Create
Promise.all(toCreate.map(function(item) {
return model.create(_.pick(_.extend({}, item, where), _.keys(model.rawAttributes)))
.then(function(obj) {
return syncRecursive(obj, item);
});
})),
// Update
Promise.all(_.values(toUpdate).map(function(obj) {
var item = collectionItems[obj.id],
newAttributes = _.pick(_.extend(_.omit(item, 'id', 'createdAt', 'updatedAt'), where), _.keys(model.rawAttributes));
return obj.updateAttributes(newAttributes)
.then(function(obj) {
return syncRecursive(obj, item);
});
})),
// Delete
Promise.all(toDelete.map(function(item) {
return item.destroy();
}))
);
});
}
syncModels: function(model, collection, where, include, keep) {
var syncDeferred = promise.defer(), // finishing the sync
chainDeferred = promise.defer(), // inserting/updating/deleting model instances
assocDeferred = {}, // syncing child relations
clientModels = {}, // id -> client model
updateModels = []; // id -> db model
include = include || [];
function normalizeCase(string) {
return string.charAt(0).toLowerCase() + string.slice(1);
}
// The next two methods are used to help us make sure child objects
// in one-to-many assocaitions are saved. When setting up the
// operation to add or update a model, we call `deferAssociations()`
// to create a deferred promise of those things having been
// inserted/updated. When models have been successfully inserted or
// updated, we call `syncAssociations()` to actually do the work
// and resolve the promise.
function deferAssociations(id, model) {
if(!model.associations)
return;
_.each(model.associations, function(association) {
// XXX: Other types not yet implemented
if(association.associationType != "HasMany")
return;
var target = association.target,
key = normalizeCase(association.options.as? association.options.as : target.tableName);
assocDeferred[id + ":" + key] = promise.defer();
});
}
function syncAssociations(id, obj, item) {
if(!obj.Model.associations)
return;
_.each(obj.Model.associations, function(association) {
// XXX: Other types not yet implemented
if(association.associationType != "HasMany")
return;
var target = association.target,
key = normalizeCase(association.options.as? association.options.as : target.tableName),
val = item[key],
where = {},
adef = assocDeferred[id + ":" + key];
if(val !== undefined) {
where[association.identifier] = obj.id;
module.exports.utils.syncModels(target, val, where, [], keep)
.then(
function() {
adef.resolve();
},
function(error) {
adef.reject(error);
}
);
}
});
}
// keep track of models we are updating
_.each(collection, function(item) {
if(item.id) {
clientModels[item.id] = item;
}
});
model.findAll({
where: where,
include: include
})
.success(function(response) {
var chainer = new Sequelize.Utils.QueryChainer();
_.each(response, function(item) {
var matchingModel = clientModels[item.id];
if(matchingModel === undefined) {
// not found in the request? destroy it
if(!keep) {
chainer.add(item.destroy());
}
} else {
// got one already? we'll need to add it
updateModels[item.id] = item;
}
});
// add new models
_.each(collection, function(item, index) {
if(!item.id || updateModels[item.id] === undefined) {
var id = "new:" + index;
deferAssociations(id, model);
chainer.add(
model.create(_.pick(_.extend({}, item, where), _.keys(model.rawAttributes)))
.success(function (obj) {
syncAssociations(id, obj, item);
})
);
}
});
// update existing ones
_.each(updateModels, function(obj) {
var id = obj.id,
clientModel = clientModels[id],
newAttributes = _.pick(_.extend(_.omit(clientModel, 'id', 'createdAt', 'updatedAt'), where), _.keys(model.rawAttributes));
deferAssociations(id, model);
chainer.add(
obj.updateAttributes(newAttributes)
.success(function(obj) {
syncAssociations(id, obj, clientModel);
})
);
});
chainer.run()
.success(function() {
chainDeferred.resolve("success");
})
.error(function(errors) {
console.error(errors);
chainDeferred.reject("database error updating records");
});
}).error(function () {
chainDeferred.reject("database error querying for records");
});
chainDeferred
.then(
function() {
// When the chainer is done, we know we'll have at least
// *created* all the deferreds for any child associations
promise.allOrNone(_.values(assocDeferred))
.then(
function() {
syncDeferred.resolve();
},
function(error) {
syncDeferred.reject(error);
}
);
},
function(error) {
syncDeferred.reject(error);
}
);
return syncDeferred.promise;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment