'use strict'; var Utils = require('./../utils') , Helpers = require('../associations/helpers') , DataTypes = require('../data-types') , Promise = require('bluebird'); var CounterCache = function(association, options) { this.association = association; this.source = association.source; this.target = association.target; this.options = options || {}; this.sequelize = this.source.modelManager.sequelize; this.as = this.options.as; if (association.associationType !== 'HasMany') { throw new Error('Can only have CounterCache on HasMany association'); } if (this.as) { this.isAliased = true; this.columnName = this.as; } else { this.as = 'count_' + this.target.options.name.plural; this.columnName = Utils._.camelizeIf( this.as, !this.source.options.underscored ); } this.injectAttributes(); this.injectHooks(); }; // Add countAssociation attribute to source model CounterCache.prototype.injectAttributes = function() { // Do not try to use a column that's already taken Helpers.checkNamingCollision(this); var newAttributes = {}; newAttributes[this.columnName] = { type: DataTypes.INTEGER, allowNull: false, defaultValue: Utils._.partial( Utils.toDefaultValue, 0 ) }; Utils.mergeDefaults(this.source.rawAttributes, newAttributes); // Sync attributes and setters/getters to DAO prototype this.source.refreshAttributes(); }; // Add setAssociaton method to the prototype of the model instance CounterCache.prototype.injectHooks = function() { var association = this.association, counterCacheInstance = this, CounterUtil, fullUpdateHook, atomicHooks, previousTargetId; CounterUtil = { update: function (targetId, options) { var query = CounterUtil._targetQuery(targetId); return association.target.count({ where: query, logging: options && options.logging }).then(function (count) { var newValues = {}; query = CounterUtil._sourceQuery(targetId); newValues[counterCacheInstance.columnName] = count; return association.source.update(newValues, { where: query, logging: options && options.logging }); }); }, increment: function (targetId, options) { var query = CounterUtil._sourceQuery(targetId); return association.source.find({ where: query, logging: options && options.logging }).then(function (instance) { return instance.increment(counterCacheInstance.columnName, { by: 1, logging: options && options.logging }); }); }, decrement: function (targetId, options) { var query = CounterUtil._sourceQuery(targetId); return association.source.find({ where: query, logging: options && options.logging }).then(function (instance) { return instance.decrement(counterCacheInstance.columnName, { by: 1, logging: options && options.logging }); }); }, // helpers _targetQuery: function (id) { var query = {}; query[association.identifier] = id; return query; }, _sourceQuery: function (id) { var query = {}; query[association.source.primaryKeyAttribute] = id; return query; } }; fullUpdateHook = function (target, options) { var targetId = target.get(association.identifier) , promises = []; if (targetId) { promises.push(CounterUtil.update(targetId, options)); } if (previousTargetId && previousTargetId !== targetId) { promises.push(CounterUtil.update(previousTargetId, options)); } return Promise.all(promises).return(undefined); }; atomicHooks = { create: function (target, options) { var targetId = target.get(association.identifier); if (targetId) { return CounterUtil.increment(targetId, options); } }, update: function (target, options) { var targetId = target.get(association.identifier) , promises = []; if (targetId && !previousTargetId) { promises.push(CounterUtil.increment(targetId, options)); } if (!targetId && previousTargetId) { promises.push(CounterUtil.decrement(targetId, options)); } if (previousTargetId && targetId && previousTargetId !== targetId) { promises.push(CounterUtil.increment(targetId, options)); promises.push(CounterUtil.decrement(previousTargetId, options)); } return Promise.all(promises); }, destroy: function (target, options) { var targetId = target.get(association.identifier); if (targetId) { return CounterUtil.decrement(targetId, options); } } }; // previousDataValues are cleared before afterUpdate, so we need to save this here association.target.addHook('beforeUpdate', function (target) { previousTargetId = target.previous(association.identifier); }); if (this.options.atomic === false) { association.target.addHook('afterCreate', fullUpdateHook); association.target.addHook('afterUpdate', fullUpdateHook); association.target.addHook('afterDestroy', fullUpdateHook); } else { association.target.addHook('afterCreate', atomicHooks.create); association.target.addHook('afterUpdate', atomicHooks.update); association.target.addHook('afterDestroy', atomicHooks.destroy); } }; module.exports = CounterCache;