has-many.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. var Utils = require("./../utils")
  2. , DataTypes = require('./../data-types')
  3. , Helpers = require('./helpers')
  4. , _ = require('lodash')
  5. , Transaction = require('../transaction')
  6. var HasManySingleLinked = require("./has-many-single-linked")
  7. , HasManyDoubleLinked = require("./has-many-double-linked")
  8. module.exports = (function() {
  9. var HasMany = function(source, target, options) {
  10. var self = this
  11. this.associationType = 'HasMany'
  12. this.source = source
  13. this.target = target
  14. this.targetAssociation = null
  15. this.options = options
  16. this.sequelize = source.daoFactoryManager.sequelize
  17. this.through = options.through
  18. this.isMultiAssociation = true
  19. this.isSelfAssociation = this.source === this.target
  20. this.doubleLinked = false
  21. this.as = this.options.as
  22. this.combinedTableName = Utils.combineTableNames(
  23. this.source.tableName,
  24. this.isSelfAssociation ? (this.as || this.target.tableName) : this.target.tableName
  25. )
  26. /*
  27. * Map joinTableModel/Name to through for BC
  28. */
  29. if (this.through === undefined) {
  30. this.through = this.options.joinTableModel || this.options.joinTableName;
  31. /*
  32. * If both are undefined, see if useJunctionTable was false (for self associations) - else assume through to be true
  33. */
  34. if (this.through === undefined) {
  35. if (this.options.useJunctionTable === false) {
  36. this.through = null;
  37. } else {
  38. this.through = true;
  39. }
  40. }
  41. }
  42. /*
  43. * Determine associationAccessor, especially for include options to identify the correct model
  44. */
  45. this.associationAccessor = this.as
  46. if (!this.associationAccessor && (typeof this.through === "string" || Object(this.through) === this.through)) {
  47. this.associationAccessor = this.through.tableName || this.through
  48. }
  49. else if (!this.associationAccessor) {
  50. this.associationAccessor = this.combinedTableName
  51. }
  52. /*
  53. * If self association, this association is target association
  54. */
  55. if (this.isSelfAssociation) {
  56. this.targetAssociation = this
  57. }
  58. /*
  59. * Else find partner DAOFactory if present, to identify double linked association
  60. */
  61. else if (this.through) {
  62. _.each(this.target.associations, function (association, accessor) {
  63. if (self.source === association.target) {
  64. var paired = false
  65. // If through is default, we determine pairing by the accesor value (i.e. DAOFactory's using as won't pair, but regular ones will)
  66. if (self.through === true && accessor === self.associationAccessor) {
  67. paired = true
  68. }
  69. // If through is not default, determine pairing by through value (model/string)
  70. if (self.through !== true && self.options.through === association.options.through) {
  71. paired = true
  72. }
  73. // If paired, set properties identifying both associations as double linked, and allow them to each eachtoerh
  74. if (paired) {
  75. self.doubleLinked = true
  76. association.doubleLinked = true
  77. self.targetAssociation = association
  78. association.targetAssociation = self
  79. }
  80. }
  81. })
  82. }
  83. /*
  84. * If we are double linked, and through is either default or a string, we create the through model and set it on both associations
  85. */
  86. if (this.doubleLinked) {
  87. if (this.through === true) {
  88. this.through = this.combinedTableName
  89. }
  90. }
  91. if (typeof this.through === "string") {
  92. this.through = this.sequelize.define(this.through, {}, _.extend(this.options, {
  93. tableName: this.through
  94. }))
  95. if (this.targetAssociation) {
  96. this.targetAssociation.through = this.through
  97. }
  98. }
  99. this.options.tableName = this.combinedName = (this.through === Object(this.through) ? this.through.tableName : this.through)
  100. if (this.as) {
  101. this.isAliased = true
  102. } else {
  103. this.as = Utils.pluralize(this.target.tableName, this.target.options.language)
  104. }
  105. this.accessors = {
  106. get: Utils._.camelize('get_' + this.as),
  107. set: Utils._.camelize('set_' + this.as),
  108. add: Utils._.camelize(Utils.singularize('add_' + this.as, this.target.options.language)),
  109. create: Utils._.camelize(Utils.singularize('create_' + this.as, this.target.options.language)),
  110. remove: Utils._.camelize(Utils.singularize('remove_' + this.as, this.target.options.language)),
  111. hasSingle: Utils._.camelize(Utils.singularize('has_' + this.as, this.target.options.language)),
  112. hasAll: Utils._.camelize('has_' + this.as)
  113. }
  114. }
  115. // the id is in the target table
  116. // or in an extra table which connects two tables
  117. HasMany.prototype.injectAttributes = function() {
  118. var doubleLinked = this.doubleLinked
  119. , self = this
  120. , primaryKeyDeleted = false
  121. this.identifier = this.options.foreignKey || Utils._.underscoredIf(Utils.singularize(this.source.tableName, this.source.options.language) + "Id", this.options.underscored)
  122. // is there already a single sided association between the source and the target?
  123. // or is the association on the model itself?
  124. if ((this.isSelfAssociation && Object(this.through) === this.through) || doubleLinked) {
  125. // remove the obsolete association identifier from the source
  126. if (this.isSelfAssociation) {
  127. this.foreignIdentifier = Utils._.underscoredIf((this.options.as || this.target.tableName) + 'Id', this.options.underscored)
  128. } else {
  129. this.foreignIdentifier = this.targetAssociation.identifier
  130. this.targetAssociation.foreignIdentifier = this.identifier
  131. if (isForeignKeyDeletionAllowedFor.call(this, this.source, this.foreignIdentifier)) {
  132. delete this.source.rawAttributes[this.foreignIdentifier]
  133. }
  134. if (isForeignKeyDeletionAllowedFor.call(this, this.target, this.identifier)) {
  135. delete this.targetAssociation.source.rawAttributes[this.identifier]
  136. }
  137. }
  138. // remove any PKs previously defined by sequelize
  139. Utils._.each(this.through.attributes, function(dataTypeString, attributeName) {
  140. if (dataTypeString.toString().indexOf('PRIMARY KEY') !== -1 && self.through.rawAttributes[attributeName]._autoGenerated === true) {
  141. delete self.through.rawAttributes[attributeName]
  142. primaryKeyDeleted = true
  143. }
  144. })
  145. // define a new model, which connects the models
  146. var combinedTableAttributes = {}
  147. var sourceKeys = Object.keys(this.source.primaryKeys);
  148. var sourceKeyType = ((!this.source.hasPrimaryKeys || sourceKeys.length !== 1) ? DataTypes.INTEGER : this.source.rawAttributes[sourceKeys[0]].type)
  149. var targetKeys = Object.keys(this.target.primaryKeys);
  150. var targetKeyType = ((!this.target.hasPrimaryKeys || targetKeys.length !== 1) ? DataTypes.INTEGER : this.target.rawAttributes[targetKeys[0]].type)
  151. if (primaryKeyDeleted) {
  152. combinedTableAttributes[this.identifier] = {type: sourceKeyType, primaryKey: true}
  153. combinedTableAttributes[this.foreignIdentifier] = {type: targetKeyType, primaryKey: true}
  154. } else {
  155. var uniqueKey = [this.through.tableName, this.identifier, this.foreignIdentifier, 'unique'].join('_')
  156. combinedTableAttributes[this.identifier] = {type: sourceKeyType, unique: uniqueKey}
  157. combinedTableAttributes[this.foreignIdentifier] = {type: targetKeyType, unique: uniqueKey}
  158. }
  159. this.through.rawAttributes = Utils._.merge(this.through.rawAttributes, combinedTableAttributes)
  160. this.through.init(this.through.daoFactoryManager)
  161. if (this.options.syncOnAssociation) {
  162. this.through.sync()
  163. }
  164. } else {
  165. var newAttributes = {}
  166. , sourceKeys = Object.keys(this.source.primaryKeys)
  167. , keyType = ((this.source.hasPrimaryKeys && sourceKeys.length === 1) ? this.source.rawAttributes[sourceKeys[0]].type : DataTypes.INTEGER)
  168. newAttributes[this.identifier] = { type: this.options.keyType || keyType}
  169. Helpers.addForeignKeyConstraints(newAttributes[this.identifier], this.source, this.target, this.options)
  170. Utils._.defaults(this.target.rawAttributes, newAttributes)
  171. }
  172. // Sync attributes and setters/getters to DAO prototype
  173. this.target.refreshAttributes()
  174. this.source.refreshAttributes()
  175. return this
  176. }
  177. HasMany.prototype.injectGetter = function(obj) {
  178. var self = this
  179. obj[this.accessors.get] = function(options) {
  180. var Class = Object(self.through) === self.through ? HasManyDoubleLinked : HasManySingleLinked
  181. return new Class(self, this).injectGetter(options)
  182. }
  183. obj[this.accessors.hasAll] = function(objects, options) {
  184. var instance = this;
  185. var customEventEmitter = new Utils.CustomEventEmitter(function() {
  186. instance[self.accessors.get](options)
  187. .error(function(err) { customEventEmitter.emit('error', err) })
  188. .success(function(associatedObjects) {
  189. customEventEmitter.emit('success',
  190. Utils._.all(objects, function(o) {
  191. return Utils._.any(associatedObjects, function(associatedObject) {
  192. return Utils._.all(associatedObject.identifiers, function(key, identifier) {
  193. return o[identifier] == associatedObject[identifier];
  194. });
  195. })
  196. })
  197. )
  198. })
  199. })
  200. return customEventEmitter.run()
  201. }
  202. obj[this.accessors.hasSingle] = function(o, options) {
  203. var instance = this
  204. var customEventEmitter = new Utils.CustomEventEmitter(function() {
  205. instance[self.accessors.get](options)
  206. .error(function(err){ customEventEmitter.emit('error', err)})
  207. .success(function(associatedObjects) {
  208. customEventEmitter.emit('success',
  209. Utils._.any(associatedObjects, function(associatedObject) {
  210. return Utils._.all(associatedObject.identifiers, function(key, identifier) {
  211. return o[identifier] == associatedObject[identifier];
  212. });
  213. })
  214. )
  215. })
  216. })
  217. return customEventEmitter.run()
  218. }
  219. return this
  220. }
  221. HasMany.prototype.injectSetter = function(obj) {
  222. var self = this
  223. obj[this.accessors.set] = function(newAssociatedObjects, defaultAttributes) {
  224. if (newAssociatedObjects === null) {
  225. newAssociatedObjects = []
  226. }
  227. var instance = this
  228. // define the returned customEventEmitter, which will emit the success event once everything is done
  229. return new Utils.CustomEventEmitter(function(emitter) {
  230. instance[self.accessors.get]({
  231. transaction: (defaultAttributes || {}).transaction
  232. })
  233. .success(function(oldAssociatedObjects) {
  234. var Class = Object(self.through) === self.through ? HasManyDoubleLinked : HasManySingleLinked
  235. new Class(self, instance).injectSetter(emitter, oldAssociatedObjects, newAssociatedObjects, defaultAttributes)
  236. })
  237. .proxy(emitter, {events: ['error', 'sql']})
  238. }).run()
  239. }
  240. obj[this.accessors.add] = function(newAssociatedObject, additionalAttributes) {
  241. var instance = this
  242. , primaryKeys = Object.keys(newAssociatedObject.daoFactory.primaryKeys || {})
  243. , primaryKey = primaryKeys.length === 1 ? primaryKeys[0] : 'id'
  244. , where = {}
  245. where[newAssociatedObject.daoFactory.tableName+'.'+primaryKey] = newAssociatedObject[primaryKey]
  246. return new Utils.CustomEventEmitter(function(emitter) {
  247. instance[self.accessors.get]({
  248. where: where,
  249. transaction: (additionalAttributes || {}).transaction
  250. })
  251. .proxy(emitter, {events: ['error', 'sql']})
  252. .success(function(currentAssociatedObjects) {
  253. if (currentAssociatedObjects.length === 0 || Object(self.through) === self.through) {
  254. var Class = Object(self.through) === self.through ? HasManyDoubleLinked : HasManySingleLinked
  255. new Class(self, instance).injectAdder(emitter, newAssociatedObject, additionalAttributes, !!currentAssociatedObjects.length)
  256. } else {
  257. emitter.emit('success', newAssociatedObject);
  258. }
  259. })
  260. }).run()
  261. }
  262. obj[this.accessors.remove] = function(oldAssociatedObject, options) {
  263. var instance = this
  264. return new Utils.CustomEventEmitter(function(emitter) {
  265. instance[self.accessors.get]({
  266. transaction: (options || {}).transaction
  267. }).success(function(currentAssociatedObjects) {
  268. var newAssociations = []
  269. , oldAssociations = []
  270. currentAssociatedObjects.forEach(function(association) {
  271. if (!Utils._.isEqual(oldAssociatedObject.identifiers, association.identifiers)) {
  272. newAssociations.push(association)
  273. }
  274. })
  275. var tick = 0
  276. var next = function(err, i) {
  277. if (!!err || i >= oldAssociations.length) {
  278. return run(err)
  279. }
  280. oldAssociations[i].destroy().error(function(err) {
  281. next(err)
  282. })
  283. .success(function() {
  284. tick++
  285. next(null, tick)
  286. })
  287. }
  288. var run = function(err) {
  289. if (!!err) {
  290. return emitter.emit('error', err)
  291. }
  292. instance[self.accessors.set](newAssociations).proxy(emitter)
  293. }
  294. if (oldAssociations.length > 0) {
  295. next(null, tick)
  296. } else {
  297. run()
  298. }
  299. })
  300. }).run()
  301. }
  302. return this
  303. }
  304. HasMany.prototype.injectCreator = function(obj) {
  305. var self = this
  306. obj[this.accessors.create] = function(values, fieldsOrOptions) {
  307. var instance = this
  308. , options = {}
  309. if ((fieldsOrOptions || {}).transaction instanceof Transaction) {
  310. options.transaction = fieldsOrOptions.transaction
  311. delete fieldsOrOptions.transaction
  312. }
  313. return new Utils.CustomEventEmitter(function(emitter) {
  314. // Create the related model instance
  315. self.target
  316. .create(values, fieldsOrOptions)
  317. .proxy(emitter, { events: ['error', 'sql'] })
  318. .success(function(newAssociatedObject) {
  319. instance[self.accessors.add](newAssociatedObject, options)
  320. .proxy(emitter)
  321. })
  322. }).run()
  323. }
  324. return this
  325. };
  326. /**
  327. * The method checks if it is ok to delete the previously defined foreign key.
  328. * This is done because we need to keep the foreign key if another association
  329. * is depending on it.
  330. *
  331. * @param {DaoFactory} daoFactory The source or target DaoFactory of this assocation
  332. * @param {[type]} identifier The name of the foreign key identifier
  333. * @return {Boolean} Whether or not the deletion of the foreign key is ok.
  334. */
  335. var isForeignKeyDeletionAllowedFor = function(daoFactory, identifier) {
  336. var isAllowed = true
  337. , associationNames = Utils._.without(Object.keys(daoFactory.associations), this.associationAccessor)
  338. associationNames.forEach(function(associationName) {
  339. if (daoFactory.associations[associationName].identifier === identifier) {
  340. isAllowed = false
  341. }
  342. })
  343. return isAllowed
  344. }
  345. return HasMany
  346. })()