PageRenderTime 56ms CodeModel.GetById 19ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/hooks/pubsub/index.js

https://github.com/carlolee/sails
JavaScript | 1458 lines | 749 code | 298 blank | 411 comment | 205 complexity | 74db6bf3e4963808ee244687e0c7fb38 MD5 | raw file
  1. /**
  2. * Module dependencies.
  3. */
  4. var util = require('util')
  5. , _ = require('lodash')
  6. , getSDKMetadata = require('../sockets/lib/getSDKMetadata')
  7. , STRINGFILE = require('sails-stringfile');
  8. /**
  9. * Module errors
  10. */
  11. var Err = {
  12. dependency: function (dependent, dependency) {
  13. return new Error( '\n' +
  14. 'Cannot use `' + dependent + '` hook ' +
  15. 'without the `' + dependency + '` hook enabled!'
  16. );
  17. }
  18. };
  19. module.exports = function(sails) {
  20. /**
  21. * Expose Hook definition
  22. */
  23. return {
  24. initialize: function(cb) {
  25. var self = this;
  26. // If `views` and `http` hook is not enabled, complain and respond w/ error
  27. if (!sails.hooks.sockets) {
  28. return cb( Err.dependency('pubsub', 'sockets') );
  29. }
  30. // Add low-level, generic socket methods. These are mostly just wrappers
  31. // around socket.io, to enforce a little abstraction.
  32. addLowLevelSocketMethods();
  33. if (!sails.hooks.orm) {
  34. return cb( Err.dependency('pubsub', 'orm') );
  35. }
  36. // Wait for `hook:orm:loaded`
  37. sails.on('hook:orm:loaded', function() {
  38. // Do the heavy lifting
  39. self.augmentModels();
  40. // Indicate that the hook is fully loaded
  41. cb();
  42. });
  43. // When the orm is reloaded, re-apply all of the pubsub methods to the
  44. // models
  45. sails.on('hook:orm:reloaded', function() {
  46. self.augmentModels();
  47. // Trigger an event in case something needs to respond to the pubsub reload
  48. sails.emit('hook:pubsub:reloaded');
  49. });
  50. },
  51. augmentModels: function() {
  52. // Augment models with room/socket logic (& bind context)
  53. for (var identity in sails.models) {
  54. var AugmentedModel = _.defaults(sails.models[identity], getPubsubMethods(), {autosubscribe: true} );
  55. _.bindAll(AugmentedModel,
  56. 'subscribe',
  57. 'watch',
  58. 'introduce',
  59. 'retire',
  60. 'unwatch',
  61. 'unsubscribe',
  62. 'publish',
  63. 'room',
  64. 'publishCreate',
  65. 'publishUpdate',
  66. 'publishDestroy',
  67. 'publishAdd',
  68. 'publishRemove'
  69. );
  70. sails.models[identity] = AugmentedModel;
  71. }
  72. }
  73. };
  74. function addLowLevelSocketMethods () {
  75. sails.sockets = {};
  76. sails.sockets.DEFAULT_EVENT_NAME = 'message';
  77. sails.sockets.subscribeToFirehose = require('./drink')(sails);
  78. sails.sockets.unsubscribeFromFirehose = require('./drink')(sails);
  79. sails.sockets.publishToFirehose = require('./squirt')(sails);
  80. /**
  81. * Subscribe a socket to a generic room
  82. * @param {object} socket The socket to subscribe.
  83. * @param {string} roomName The room to subscribe to
  84. */
  85. sails.sockets.join = function(sockets, roomName) {
  86. if (!sails.util.isArray(sockets)) {
  87. sockets = [sockets];
  88. }
  89. sails.util.each(sockets, function(socket) {
  90. // If a string was sent, try to look up a socket with that ID
  91. if (typeof socket == 'string') {socket = sails.io.sockets.socket(socket);}
  92. // If it's not a valid socket object, bail
  93. if (! (socket && socket.manager) ) return;
  94. // Join up!
  95. socket.join(roomName);
  96. });
  97. return true;
  98. };
  99. /**
  100. * Unsubscribe a socket from a generic room
  101. * @param {object} socket The socket to unsubscribe.
  102. * @param {string} roomName The room to unsubscribe from
  103. */
  104. sails.sockets.leave = function(sockets, roomName) {
  105. if (!sails.util.isArray(sockets)) {
  106. sockets = [sockets];
  107. }
  108. sails.util.each(sockets, function(socket) {
  109. // If a string was sent, try to look up a socket with that ID
  110. if (typeof socket == 'string') {socket = sails.io.sockets.socket(socket);}
  111. // If it's not a valid socket object, bail
  112. if (! (socket && socket.manager) ) return;
  113. // See ya!
  114. socket.leave(roomName);
  115. });
  116. return true;
  117. };
  118. /**
  119. * Broadcast a message to a room
  120. *
  121. * If the event name is omitted, "message" will be used by default.
  122. * Thus, sails.sockets.broadcast(roomName, data) is also a valid usage.
  123. *
  124. * @param {string} roomName The room to broadcast a message to
  125. * @param {string} eventName The event name to broadcast
  126. * @param {object} data The data to broadcast
  127. * @param {object} socket Optional socket to omit
  128. */
  129. sails.sockets.broadcast = function(roomName, eventName, data, socketToOmit) {
  130. // If the 'eventName' is an object, assume the argument was omitted and
  131. // parse it as data instead.
  132. if (typeof eventName === 'object') {
  133. data = eventName;
  134. eventName = null;
  135. }
  136. // Default to the sails.sockets.DEFAULT_EVENT_NAME.
  137. if (!eventName) {
  138. eventName = sails.sockets.DEFAULT_EVENT_NAME;
  139. }
  140. // If we were given a valid socket to omit, broadcast from there.
  141. if (socketToOmit && socketToOmit.manager) {
  142. socketToOmit.broadcast.to(roomName).emit(eventName, data);
  143. }
  144. // Otherwise broadcast to everyone
  145. else {
  146. sails.io.sockets.in(roomName).emit(eventName, data);
  147. }
  148. };
  149. /**
  150. * Broadcast a message to all connected sockets
  151. *
  152. * If the event name is omitted, sails.sockets.DEFAULT_EVENT_NAME will be used by default.
  153. * Thus, sails.sockets.blast(data) is also a valid usage.
  154. *
  155. * @param {string} event The event name to broadcast
  156. * @param {object} data The data to broadcast
  157. * @param {object} socket Optional socket to omit
  158. */
  159. sails.sockets.blast = function(eventName, data, socketToOmit) {
  160. // If the 'eventName' is an object, assume the argument was omitted and
  161. // parse it as data instead.
  162. if (typeof eventName === 'object') {
  163. data = eventName;
  164. eventName = null;
  165. }
  166. // Default to the sails.sockets.DEFAULT_EVENT_NAME eventName.
  167. if (!eventName) {
  168. eventName = sails.sockets.DEFAULT_EVENT_NAME;
  169. }
  170. // If we were given a valid socket to omit, broadcast from there.
  171. if (socketToOmit && socketToOmit.manager) {
  172. socketToOmit.broadcast.emit(eventName, data);
  173. }
  174. // Otherwise broadcast to everyone
  175. else {
  176. sails.io.sockets.emit(eventName, data);
  177. }
  178. };
  179. /**
  180. * Get the ID of a socket object
  181. * @param {object} socket The socket object to get the ID of
  182. * @return {string} The socket's ID
  183. */
  184. sails.sockets.id = function(socket) {
  185. // If a request was passed in, get its socket
  186. socket = socket.socket || socket;
  187. if (socket) {
  188. return socket.id;
  189. }
  190. else return undefined;
  191. };
  192. /**
  193. * Emit a message to one or more sockets by ID
  194. *
  195. * If the event name is omitted, "message" will be used by default.
  196. * Thus, sails.sockets.emit(socketIDs, data) is also a valid usage.
  197. *
  198. * @param {array|string} socketIDs The ID or IDs of sockets to send a message to
  199. * @param {string} event The name of the message to send
  200. * @param {object} data Optional data to send with the message
  201. */
  202. sails.sockets.emit = function(socketIDs, eventName, data) {
  203. if (!_.isArray(socketIDs)) {
  204. socketIDs = [socketIDs];
  205. }
  206. if (typeof eventName === 'object') {
  207. data = eventName;
  208. eventName = null;
  209. }
  210. if (!eventName) {
  211. eventName = sails.sockets.DEFAULT_EVENT_NAME;
  212. }
  213. _.each(socketIDs, function(socketID) {
  214. sails.io.sockets.socket(socketID).emit(eventName, data);
  215. });
  216. };
  217. /**
  218. * Get the list of IDs of sockets subscribed to a room
  219. * @param {string} roomName The room to get subscribers of
  220. * @return {array} An array of socket instances
  221. */
  222. sails.sockets.subscribers = function(roomName) {
  223. return _.pluck(sails.io.sockets.clients(roomName), 'id');
  224. };
  225. /**
  226. * Get the list of rooms a socket is subscribed to
  227. * @param {object} socket The socket to get rooms for
  228. * @return {array} An array of room names
  229. */
  230. sails.sockets.socketRooms = function(socket) {
  231. return _.map(_.keys(sails.io.sockets.manager.roomClients[socket.id]), function(roomName) {return roomName.replace(/^\//,'');});
  232. };
  233. /**
  234. * Get the list of all rooms
  235. * @return {array} An array of room names, minus the empty room
  236. */
  237. sails.sockets.rooms = function() {
  238. var rooms = sails.util.clone(sails.io.sockets.manager.rooms);
  239. delete rooms[""];
  240. return sails.util.map(sails.util.keys(rooms), function(room){return room.substr(1);});
  241. };
  242. }
  243. /**
  244. * These methods get appended to the Model class objects
  245. * Some take req.socket as an argument to get access
  246. * to user('s|s') socket object(s)
  247. */
  248. function getPubsubMethods () {
  249. return {
  250. /**
  251. * Broadcast a message to a room
  252. *
  253. * Wrapper for sails.sockets.broadcast
  254. * Can be overridden at a model level, i.e. for encapsulating messages within a single event name.
  255. *
  256. * @param {string} roomName The room to broadcast a message to
  257. * @param {string} eventName The event name to broadcast
  258. * @param {object} data The data to broadcast
  259. * @param {object} socket Optional socket to omit
  260. *
  261. * @api private
  262. */
  263. broadcast: function(roomName, eventName, data, socketToOmit) {
  264. sails.sockets.broadcast(roomName, eventName, data, socketToOmit);
  265. },
  266. /**
  267. * TODO: document
  268. */
  269. getAllContexts: function() {
  270. var contexts = ['update', 'destroy', 'message'];
  271. _.each(this.associations, function(association) {
  272. if (association.type == 'collection') {
  273. contexts.push('add:'+association.alias);
  274. contexts.push('remove:'+association.alias);
  275. }
  276. });
  277. return contexts;
  278. },
  279. /**
  280. * Broadcast a custom message to sockets connected to the specified models
  281. * @param {Object|String|Finite} record -- record or ID of record whose subscribers should receive the message
  282. * @param {Object|Array|String|Finite} message -- the message payload
  283. * @param {Request|Socket} req - if specified, broadcast using this
  284. * socket (effectively omitting it)
  285. *
  286. */
  287. message: function(record, data, req) {
  288. // If a request object was sent, get its socket, otherwise assume a socket was sent.
  289. var socketToOmit = (req && req.socket ? req.socket : req);
  290. // If no records provided, throw an error
  291. if (!record) {
  292. return sails.log.error(
  293. util.format(
  294. 'Must specify a record or record ID when calling `Model.publish` '+
  295. '(you specified: `%s`)', record));
  296. }
  297. // Otherwise publish to each instance room
  298. else {
  299. // Get the record ID (if the record argument isn't already a scalar)
  300. var id = record[this.primaryKey] || record;
  301. // Get the socket room to publish to
  302. var room = this.room(id, "message");
  303. // Create the payload
  304. var payload = {
  305. verb: "messaged",
  306. id: id,
  307. data: data
  308. };
  309. this.broadcast( room, this.identity, payload, socketToOmit );
  310. sails.log.silly("Published message to ", room, ": ", payload);
  311. }
  312. },
  313. /**
  314. * Broadcast a message to sockets connected to the specified models
  315. * (or null to broadcast to the entire class room)
  316. *
  317. * @param {Object|Array|String|Finite} models -- models whose subscribers should receive the message
  318. * @param {String} eventName -- the event name to broadcast with
  319. * @param {String} context -- the context to broadcast to
  320. * @param {Object|Array|String|Finite} data -- the message payload
  321. * socket (effectively omitting it)
  322. *
  323. * @api private
  324. */
  325. publish: function (models, eventName, context, data, req) {
  326. var self = this;
  327. // If the event name is an object, assume we're seeing `publish(models, data, req)`
  328. if (typeof eventName === 'object') {
  329. req = context;
  330. context = null;
  331. data = eventName;
  332. eventName = null;
  333. }
  334. // Default to the event name being the model identity
  335. if (!eventName) {
  336. eventName = this.identity;
  337. }
  338. // If the context is an object, assume we're seeing `publish(models, eventName, data, req)`
  339. if (typeof context === 'object' && context !== null) {
  340. req = data;
  341. data = context;
  342. context = null;
  343. }
  344. // Default to using the message context
  345. if (!context) {
  346. sails.log.warn('`Model.publish` should specify a context; defaulting to "message". Try `Model.message` instead?');
  347. context = 'message';
  348. }
  349. // If a request object was sent, get its socket, otherwise assume a socket was sent.
  350. var socketToOmit = (req && req.socket ? req.socket : req);
  351. // If no models provided, publish to the class room
  352. if (!models) {
  353. STRINGFILE.logDeprecationNotice(
  354. 'Model.publish(null, ...)',
  355. STRINGFILE.get('links.docs.sockets.pubsub'),
  356. sails.log.debug) &&
  357. STRINGFILE.logUpgradeNotice(STRINGFILE.get('upgrade.classrooms'), [], sails.log.debug);
  358. sails.log.silly('Published ', eventName, ' to ', self.classRoom());
  359. self.broadcast( self.classRoom(), eventName, data, socketToOmit );
  360. return;
  361. }
  362. // Otherwise publish to each instance room
  363. else {
  364. models = this.pluralize(models);
  365. var ids = _.pluck(models, this.primaryKey);
  366. if ( ids.length === 0 ) {
  367. sails.log.warn('Can\'t publish a message to an empty list of instances-- ignoring...');
  368. }
  369. _.each(ids,function eachInstance (id) {
  370. var room = self.room(id, context);
  371. sails.log.silly("Published ", eventName, " to ", room);
  372. self.broadcast( room, eventName, data, socketToOmit );
  373. // Also broadcasts a message to the legacy instance room (derived by
  374. // using the `legacy_v0.9` context).
  375. // Uses traditional eventName === "message".
  376. // Uses traditional message format.
  377. if (sails.config.sockets['backwardsCompatibilityFor0.9SocketClients']) {
  378. var legacyRoom = self.room(id, 'legacy_v0.9');
  379. var legacyMsg = _.cloneDeep(data);
  380. legacyMsg.model = self.identity;
  381. if (legacyMsg.verb === 'created') { legacyMsg.verb = 'create'; }
  382. if (legacyMsg.verb === 'updated') { legacyMsg.verb = 'update'; }
  383. if (legacyMsg.verb === 'destroyed') { legacyMsg.verb = 'destroy'; }
  384. self.broadcast( legacyRoom, 'message', legacyMsg, socketToOmit );
  385. }
  386. });
  387. }
  388. },
  389. /**
  390. * Check that models are a list, if not, make them a list
  391. * Also if they are ids, make them dummy objects with an `id` property
  392. *
  393. * @param {Object|Array|String|Finite} models
  394. * @returns {Array} array of things that have an `id` property
  395. *
  396. * @api private
  397. * @synchronous
  398. */
  399. pluralize: function (models) {
  400. // If `models` is a non-array object,
  401. // turn it into a single-item array ("pluralize" it)
  402. // e.g. { id: 7 } -----> [ { id: 7 } ]
  403. if ( !_.isArray(models) ) {
  404. var model = models;
  405. models = [model];
  406. }
  407. // If a list of ids things look ids (finite numbers or strings),
  408. // wrap them up as dummy objects; e.g. [1,2] ---> [ {id: 1}, {id: 2} ]
  409. var self = this;
  410. return _.map(models, function (model) {
  411. if ( _.isString(model) || _.isFinite(model) ) {
  412. var id = model;
  413. var data = {};
  414. data[self.primaryKey] = id;
  415. return data;
  416. }
  417. return model;
  418. });
  419. },
  420. /**
  421. * @param {String|} id
  422. * @return {String} name of the instance room for an instance of this model w/ given id
  423. * @synchronous
  424. */
  425. room: function (id, context) {
  426. if (!id) return sails.log.error('Must specify an `id` when calling `Model.room(id)`');
  427. return 'sails_model_'+this.identity+'_'+id+':'+context;
  428. },
  429. classRoom: function () {
  430. STRINGFILE.logDeprecationNotice(
  431. 'Model.classRoom',
  432. STRINGFILE.get('links.docs.sockets.pubsub'),
  433. sails.log.debug) &&
  434. STRINGFILE.logUpgradeNotice(STRINGFILE.get('upgrade.classrooms'), [], sails.log.debug);
  435. return this._classRoom();
  436. },
  437. /**
  438. * @return {String} name of this model's global class room
  439. * @synchronous
  440. * @api private
  441. */
  442. _classRoom: function() {
  443. return 'sails_model_create_'+this.identity;
  444. },
  445. /**
  446. * Return the set of sockets subscribed to this instance
  447. * @param {String|Integer} id
  448. * @return {Array[String]}
  449. * @synchronous
  450. * @api private
  451. */
  452. subscribers: function (id, context) {
  453. // For a single context, return just the socket subscribed to that context
  454. if (context) {
  455. return sails.sockets.subscribers(this.room(id, context));
  456. }
  457. // Otherwise return the unique set of sockets subscribed to ALL contexts
  458. //
  459. // TODO: handle custom contexts here, which aren't returned by getAllContexts
  460. // Not currently a big issue since `publish` is a private API, so subscribing
  461. // to a custom context doesn't let you do much.
  462. var contexts = this.getAllContexts();
  463. var subscribers = [];
  464. _.each(contexts, function(context) {
  465. subscribers = _.union(subscribers, this.subscribers(id, context));
  466. }, this);
  467. return _.uniq(subscribers);
  468. },
  469. /**
  470. * Return the set of sockets subscribed to this class room
  471. * @return {Array[String]}
  472. * @synchronous
  473. * @api private
  474. */
  475. watchers: function() {
  476. return sails.sockets.subscribers(this._classRoom());
  477. },
  478. /**
  479. * Subscribe a socket to a handful of records in this model
  480. *
  481. * Usage:
  482. * Model.subscribe(req,socket [, records] )
  483. *
  484. * @param {Request|Socket} req - request containing the socket to subscribe, or the socket itself
  485. * @param {Object|Array|String|Finite} records - id, array of ids, model, or array of records
  486. *
  487. * e.g.
  488. * // Subscribe to User.create()
  489. * User.subscribe(req.socket)
  490. *
  491. * // Subscribe to User.update() and User.destroy()
  492. * // for the specified instances (or user.save() / user.destroy())
  493. * User.subscribe(req.socket, users)
  494. *
  495. * @api public
  496. */
  497. subscribe: function (req, records, contexts) {
  498. // If a request object was sent, get its socket, otherwise assume a socket was sent.
  499. var socket = req.socket ? req.socket : req;
  500. // todo: consider using the following instead (to lessen potential confusion w/ http requests)
  501. // (however it would need to be applied to all occurances of `req` in methods in this hook)
  502. // if (!socket.manager || !(req.socket && req.socket.manager)) {
  503. // console.log('-----',req,'\n\n---------\n\n');
  504. // sails.log.verbose('`Model.subscribe()` called by a non-socket request. Only requests originating from a connected socket may be subscribed. Ignoring...');
  505. // return;
  506. // }
  507. var self = this;
  508. // Subscribe to class room to hear about new records
  509. if (!records) {
  510. STRINGFILE.logDeprecationNotice(
  511. 'Model.subscribe(socket, null, ...)',
  512. STRINGFILE.get('links.docs.sockets.pubsub'),
  513. sails.log.debug) &&
  514. STRINGFILE.logUpgradeNotice(STRINGFILE.get('upgrade.classrooms'), [], sails.log.debug);
  515. this.watch(req);
  516. return;
  517. }
  518. contexts = contexts || this.autosubscribe;
  519. if (!contexts) {
  520. sails.log.warn("`subscribe` called without context on a model with autosubscribe:false. No action will be taken.");
  521. return;
  522. }
  523. if (contexts === true || contexts == '*') {
  524. contexts = this.getAllContexts();
  525. } else if (sails.util.isString(contexts)) {
  526. contexts = [contexts];
  527. }
  528. // If the subscribing socket is using the legacy (v0.9.x) socket SDK (sails.io.js),
  529. // always subscribe the client to the `legacy_v0.9` context.
  530. if (sails.config.sockets['backwardsCompatibilityFor0.9SocketClients'] && socket.handshake) {
  531. var sdk = getSDKMetadata(socket.handshake);
  532. var isLegacySocketClient = sdk.version === '0.9.0';
  533. if (isLegacySocketClient) {
  534. contexts.push('legacy_v0.9');
  535. }
  536. }
  537. // Subscribe to model instances
  538. records = self.pluralize(records);
  539. var ids = _.pluck(records, this.primaryKey);
  540. _.each(ids,function (id) {
  541. _.each(contexts, function(context) {
  542. sails.log.silly(
  543. 'Subscribed to the ' +
  544. self.globalId + ' with id=' + id + '\t(room :: ' + self.room(id, context) + ')'
  545. );
  546. sails.sockets.join( socket, self.room(id, context) );
  547. });
  548. });
  549. },
  550. /**
  551. * Unsubscribe a socket from some records
  552. *
  553. * @param {Request|Socket} req - request containing the socket to unsubscribe, or the socket itself
  554. * @param {Object|Array|String|Finite} models - id, array of ids, model, or array of models
  555. */
  556. unsubscribe: function (req, records, contexts) {
  557. // If a request object was sent, get its socket, otherwise assume a socket was sent.
  558. var socket = req.socket ? req.socket : req;
  559. var self = this;
  560. // If no records provided, unsubscribe from the class room
  561. if (!records) {
  562. STRINGFILE.logDeprecationNotice(
  563. 'Model.unsubscribe(socket, null, ...)',
  564. STRINGFILE.get('links.docs.sockets.pubsub'),
  565. sails.log.debug) &&
  566. STRINGFILE.logUpgradeNotice(STRINGFILE.get('upgrade.classrooms'), [], sails.log.debug);
  567. this.unwatch();
  568. }
  569. contexts = contexts || this.getAllContexts();
  570. if (contexts === true) {
  571. contexts = this.getAllContexts();
  572. }
  573. if (sails.config.sockets['backwardsCompatibilityFor0.9SocketClients'] && socket.handshake) {
  574. var sdk = getSDKMetadata(socket.handshake);
  575. var isLegacySocketClient = sdk.version === '0.9.0';
  576. if (isLegacySocketClient) {
  577. contexts.push('legacy_v0.9');
  578. }
  579. }
  580. records = self.pluralize(records);
  581. var ids = _.pluck(records, this.primaryKey);
  582. _.each(ids,function (id) {
  583. _.each(contexts, function(context) {
  584. sails.log.silly(
  585. 'Unsubscribed from the ' +
  586. self.globalId + ' with id=' + id + '\t(room :: ' + self.room(id, context) + ')'
  587. );
  588. sails.sockets.leave( socket, self.room(id, context));
  589. });
  590. });
  591. },
  592. /**
  593. * Publish an update on a particular model
  594. *
  595. * @param {String|Finite} id
  596. * - primary key of the instance we're referring to
  597. *
  598. * @param {Object} changes
  599. * - an object of changes to this instance that will be broadcasted
  600. *
  601. * @param {Request|Socket} req - if specified, broadcast using this socket (effectively omitting it)
  602. *
  603. * @api public
  604. */
  605. publishUpdate: function (id, changes, req, options) {
  606. // Make sure there's an options object
  607. options = options || {};
  608. // Ensure that we're working with a clean, unencumbered object
  609. changes = _.clone(changes);
  610. // Enforce valid usage
  611. var validId = _.isString(id) || _.isFinite(id);
  612. if ( !validId ) {
  613. return sails.log.error(
  614. 'Invalid usage of ' +
  615. '`' + this.identity + '.publishUpdate(id, changes, [socketToOmit])`'
  616. );
  617. }
  618. if (sails.util.isFunction(this.beforePublishUpdate)) {
  619. this.beforePublishUpdate(id, changes, req, options);
  620. }
  621. var data = {
  622. model: this.identity,
  623. verb: 'update',
  624. data: changes,
  625. id: id
  626. };
  627. if (options.previous && !options.noReverse) {
  628. var previous = options.previous;
  629. // If any of the changes were to association attributes, publish add or remove messages.
  630. _.each(changes, function(val, key) {
  631. // If value wasn't changed, do nothing
  632. if (val == previous[key]) return;
  633. // Find an association matching this attribute
  634. var association = _.find(this.associations, {alias: key});
  635. // If the attribute isn't an assoctiation, return
  636. if (!association) return;
  637. // Get the associated model class
  638. var ReferencedModel = sails.models[association.type == 'model' ? association.model : association.collection];
  639. // Bail if this attribute isn't in the model's schema
  640. if (association.type == 'model') {
  641. var previousPK = _.isObject(previous[key]) ? previous[key][ReferencedModel.primaryKey] : previous[key];
  642. var newPK = _.isObject(val) ? val[this.primaryKey] : val;
  643. if (previousPK == newPK) return;
  644. // Get the inverse association definition, if any
  645. reverseAssociation = _.find(ReferencedModel.associations, {collection: this.identity, via: key}) || _.find(ReferencedModel.associations, {model: this.identity, via: key});
  646. if (!reverseAssociation) {return;}
  647. // If this is a to-many association, do publishAdd or publishRemove as necessary
  648. // on the other side
  649. if (reverseAssociation.type == 'collection') {
  650. // If there was a previous value, alert the previously associated model
  651. if (previous[key]) {
  652. ReferencedModel.publishRemove(previousPK, reverseAssociation.alias, id, {noReverse:true});
  653. }
  654. // If there's a new value (i.e. it's not null), alert the newly associated model
  655. if (val) {
  656. ReferencedModel.publishAdd(newPK, reverseAssociation.alias, id, {noReverse:true});
  657. }
  658. }
  659. // Otherwise do a publishUpdate
  660. else {
  661. var pubData = {};
  662. // If there was a previous association, notify it that it has been nullified
  663. if (previous[key]) {
  664. pubData[reverseAssociation.alias] = null;
  665. ReferencedModel.publishUpdate(previousPK, pubData, req, {noReverse:true});
  666. }
  667. // If there's a new association, notify it that it has been linked
  668. if (val) {
  669. pubData[reverseAssociation.alias] = id;
  670. ReferencedModel.publishUpdate(newPK, pubData, req, {noReverse:true});
  671. }
  672. }
  673. }
  674. else {
  675. // Get the reverse association definition, if any
  676. reverseAssociation = _.find(ReferencedModel.associations, {collection: this.identity, via: key}) || _.find(ReferencedModel.associations, {model: this.identity, alias: association.via});
  677. if (!reverseAssociation) {return;}
  678. // If we can't get the previous PKs (b/c previous isn't populated), bail
  679. if (typeof(previous[key]) == 'undefined') return;
  680. // Get the previous set of IDs
  681. var previousPKs = _.pluck(previous[key], ReferencedModel.primaryKey);
  682. // Get the current set of IDs
  683. var updatedPKs = _.map(val, function(_val) {
  684. if (_.isObject(_val)) {
  685. return _val[ReferencedModel.primaryKey];
  686. } else {
  687. return _val;
  688. }
  689. });
  690. // Find any values that were added to the collection
  691. var addedPKs = _.difference(updatedPKs, previousPKs);
  692. // Find any values that were removed from the collection
  693. var removedPKs = _.difference(previousPKs, updatedPKs);
  694. // If this is a to-many association, do publishAdd or publishRemove as necessary
  695. // on the other side
  696. if (reverseAssociation.type == 'collection') {
  697. // Alert any removed models
  698. _.each(removedPKs, function(pk) {
  699. ReferencedModel.publishRemove(pk, reverseAssociation.alias, id, {noReverse:true});
  700. });
  701. // Alert any added models
  702. _.each(addedPKs, function(pk) {
  703. ReferencedModel.publishAdd(pk, reverseAssociation.alias, id, {noReverse:true});
  704. });
  705. }
  706. // Otherwise do a publishUpdate
  707. else {
  708. // Alert any removed models
  709. _.each(removedPKs, function(pk) {
  710. var pubData = {};
  711. pubData[reverseAssociation.alias] = null;
  712. ReferencedModel.publishUpdate(pk, pubData, req, {noReverse:true});
  713. });
  714. // Alert any added models
  715. _.each(addedPKs, function(pk) {
  716. var pubData = {};
  717. pubData[reverseAssociation.alias] = id;
  718. ReferencedModel.publishUpdate(pk, pubData, req, {noReverse:true});
  719. });
  720. }
  721. }
  722. }, this);
  723. }
  724. // If a request object was sent, get its socket, otherwise assume a socket was sent.
  725. var socketToOmit = (req && req.socket ? req.socket : req);
  726. // In development environment, blast out a message to everyone
  727. if (sails.config.environment == 'development') {
  728. sails.sockets.publishToFirehose(data);
  729. }
  730. data.verb = 'updated';
  731. data.previous = options.previous;
  732. delete data.model;
  733. // Broadcast to the model instance room
  734. this.publish(id, this.identity, 'update', data, socketToOmit);
  735. if (sails.util.isFunction(this.afterPublishUpdate)) {
  736. this.afterPublishUpdate(id, changes, req, options);
  737. }
  738. },
  739. /**
  740. * Publish the destruction of a particular model
  741. *
  742. * @param {String|Finite} id
  743. * - primary key of the instance we're referring to
  744. *
  745. * @param {Request|Socket} req - if specified, broadcast using this socket (effectively omitting it)
  746. *
  747. */
  748. publishDestroy: function (id, req, options) {
  749. options = options || {};
  750. // Enforce valid usage
  751. var invalidId = !id || _.isObject(id);
  752. if ( invalidId ) {
  753. return sails.log.error(
  754. 'Invalid usage of ' + this.identity +
  755. '`publishDestroy(id, [socketToOmit])`'
  756. );
  757. }
  758. if (sails.util.isFunction(this.beforePublishDestroy)) {
  759. this.beforePublishDestroy(id, req, options);
  760. }
  761. var data = {
  762. model: this.identity,
  763. verb: 'destroy',
  764. id: id,
  765. previous: options.previous
  766. };
  767. // If a request object was sent, get its socket, otherwise assume a socket was sent.
  768. var socketToOmit = (req && req.socket ? req.socket : req);
  769. // In development environment, blast out a message to everyone
  770. if (sails.config.environment == 'development') {
  771. sails.sockets.publishToFirehose(data);
  772. }
  773. data.verb = 'destroyed';
  774. delete data.model;
  775. // Broadcast to the model instance room
  776. this.publish(id, this.identity, 'destroy', data, socketToOmit);
  777. // Unsubscribe everyone from the model instance
  778. this.retire(id);
  779. if (options.previous) {
  780. var previous = options.previous;
  781. // Loop through associations and alert as necessary
  782. _.each(this.associations, function(association) {
  783. var ReferencedModel;
  784. // If it's a to-one association, and it wasn't falsy, alert
  785. // the reverse side
  786. if (association.type == 'model' && [association.alias] && previous[association.alias]) {
  787. ReferencedModel = sails.models[association.model];
  788. // Get the inverse association definition, if any
  789. reverseAssociation = _.find(ReferencedModel.associations, {collection: this.identity}) || _.find(ReferencedModel.associations, {model: this.identity});
  790. if (reverseAssociation) {
  791. // If it's a to-one, publish a simple update alert
  792. var referencedModelId = _.isObject(previous[association.alias]) ? previous[association.alias][ReferencedModel.primaryKey] : previous[association.alias];
  793. if (reverseAssociation.type == 'model') {
  794. var pubData = {};
  795. pubData[reverseAssociation.alias] = null;
  796. ReferencedModel.publishUpdate(referencedModelId, pubData, {noReverse:true});
  797. }
  798. // If it's a to-many, publish a "removed" alert
  799. else {
  800. ReferencedModel.publishRemove(referencedModelId, reverseAssociation.alias, id, req, {noReverse:true});
  801. }
  802. }
  803. }
  804. else if (association.type == 'collection' && previous[association.alias].length) {
  805. ReferencedModel = sails.models[association.collection];
  806. // Get the inverse association definition, if any
  807. reverseAssociation = _.find(ReferencedModel.associations, {collection: this.identity}) || _.find(ReferencedModel.associations, {model: this.identity});
  808. if (reverseAssociation) {
  809. _.each(previous[association.alias], function(associatedModel) {
  810. // If it's a to-one, publish a simple update alert
  811. if (reverseAssociation.type == 'model') {
  812. var pubData = {};
  813. pubData[reverseAssociation.alias] = null;
  814. ReferencedModel.publishUpdate(associatedModel[ReferencedModel.primaryKey], pubData, req, {noReverse:true});
  815. }
  816. // If it's a to-many, publish a "removed" alert
  817. else {
  818. ReferencedModel.publishRemove(associatedModel[ReferencedModel.primaryKey], reverseAssociation.alias, id, req, {noReverse:true});
  819. }
  820. });
  821. }
  822. }
  823. }, this);
  824. }
  825. if (sails.util.isFunction(this.afterPublishDestroy)) {
  826. this.afterPublishDestroy(id, req, options);
  827. }
  828. },
  829. /**
  830. * publishAdd
  831. *
  832. * @param {[type]} id [description]
  833. * @param {[type]} alias [description]
  834. * @param {[type]} idAdded [description]
  835. * @param {[type]} socketToOmit [description]
  836. */
  837. publishAdd: function(id, alias, idAdded, req, options) {
  838. // Make sure there's an options object
  839. options = options || {};
  840. // Enforce valid usage
  841. var invalidId = !id || _.isObject(id);
  842. var invalidAlias = !alias || !_.isString(alias);
  843. var invalidAddedId = !idAdded || _.isObject(idAdded);
  844. if ( invalidId || invalidAlias || invalidAddedId ) {
  845. return sails.log.error(
  846. 'Invalid usage of ' + this.identity +
  847. '`publishAdd(id, alias, idAdded, [socketToOmit])`'
  848. );
  849. }
  850. if (sails.util.isFunction(this.beforePublishAdd)) {
  851. this.beforePublishAdd(id, alias, idAdded, req);
  852. }
  853. // If a request object was sent, get its socket, otherwise assume a socket was sent.
  854. var socketToOmit = (req && req.socket ? req.socket : req);
  855. // In development environment, blast out a message to everyone
  856. if (sails.config.environment == 'development') {
  857. sails.sockets.publishToFirehose({
  858. id: id,
  859. model: this.identity,
  860. verb: 'addedTo',
  861. attribute: alias,
  862. addedId: idAdded
  863. });
  864. }
  865. this.publish(id, this.identity, 'add:'+alias, {
  866. id: id,
  867. verb: 'addedTo',
  868. attribute: alias,
  869. addedId: idAdded
  870. }, socketToOmit);
  871. if (!options.noReverse) {
  872. // Get the reverse association
  873. var reverseModel = sails.models[_.find(this.associations, {alias: alias}).collection];
  874. var data;
  875. // Subscribe to the model you're adding
  876. if (req) {
  877. data = {};
  878. data[reverseModel.primaryKey] = idAdded;
  879. reverseModel.subscribe(req, data);
  880. }
  881. // Find the reverse association, if any
  882. var reverseAssociation = _.find(reverseModel.associations, {alias: _.find(this.associations, {alias: alias}).via}) ;
  883. if (reverseAssociation) {
  884. // If this is a many-to-many association, do a publishAdd for the
  885. // other side.
  886. if (reverseAssociation.type == 'collection') {
  887. reverseModel.publishAdd(idAdded, reverseAssociation.alias, id, req, {noReverse:true});
  888. }
  889. // Otherwise, do a publishUpdate
  890. else {
  891. data = {};
  892. data[reverseAssociation.alias] = id;
  893. reverseModel.publishUpdate(idAdded, data, req, {noReverse:true});
  894. }
  895. }
  896. }
  897. if (sails.util.isFunction(this.afterPublishAdd)) {
  898. this.afterPublishAdd(id, alias, idAdded, req);
  899. }
  900. },
  901. /**
  902. * publishRemove
  903. *
  904. * @param {[type]} id [description]
  905. * @param {[type]} alias [description]
  906. * @param {[type]} idRemoved [description]
  907. * @param {[type]} socketToOmit [description]
  908. */
  909. publishRemove: function(id, alias, idRemoved, req, options) {
  910. // Make sure there's an options object
  911. options = options || {};
  912. // Enforce valid usage
  913. var invalidId = !id || _.isObject(id);
  914. var invalidAlias = !alias || !_.isString(alias);
  915. var invalidRemovedId = !idRemoved || _.isObject(idRemoved);
  916. if ( invalidId || invalidAlias || invalidRemovedId ) {
  917. return sails.log.error(
  918. 'Invalid usage of ' + this.identity +
  919. '`publishRemove(id, alias, idRemoved, [socketToOmit])`'
  920. );
  921. }
  922. if (sails.util.isFunction(this.beforePublishRemove)) {
  923. this.beforePublishRemove(id, alias, idRemoved, req);
  924. }
  925. // If a request object was sent, get its socket, otherwise assume a socket was sent.
  926. var socketToOmit = (req && req.socket ? req.socket : req);
  927. // In development environment, blast out a message to everyone
  928. if (sails.config.environment == 'development') {
  929. sails.sockets.publishToFirehose({
  930. id: id,
  931. model: this.identity,
  932. verb: 'removedFrom',
  933. attribute: alias,
  934. removedId: idRemoved
  935. });
  936. }
  937. this.publish(id, this.identity, 'remove:' + alias, {
  938. id: id,
  939. verb: 'removedFrom',
  940. attribute: alias,
  941. removedId: idRemoved
  942. }, socketToOmit);
  943. if (!options.noReverse) {
  944. // Get the reverse association, if any
  945. var reverseModel = sails.models[_.find(this.associations, {alias: alias}).collection];
  946. var reverseAssociation = _.find(reverseModel.associations, {alias: _.find(this.associations, {alias: alias}).via});
  947. if (reverseAssociation) {
  948. // If this is a many-to-many association, do a publishAdd for the
  949. // other side.
  950. if (reverseAssociation.type == 'collection') {
  951. reverseModel.publishRemove(idRemoved, reverseAssociation.alias, id, req, {noReverse:true});
  952. }
  953. // Otherwise, do a publishUpdate
  954. else {
  955. var data = {};
  956. data[reverseAssociation.alias] = null;
  957. reverseModel.publishUpdate(idRemoved, data, req, {noReverse:true});
  958. }
  959. }
  960. }
  961. if (sails.util.isFunction(this.afterPublishRemove)) {
  962. this.afterPublishRemove(id, alias, idRemoved, req);
  963. }
  964. },
  965. /**
  966. * Publish the creation of a model
  967. *
  968. * @param {Object} values
  969. * - the data to publish
  970. *
  971. * @param {Request|Socket} req - if specified, broadcast using this socket (effectively omitting it)
  972. * @api private
  973. */
  974. publishCreate: function(values, req, options) {
  975. var self = this;
  976. options = options || {};
  977. if (!values[this.primaryKey]) {
  978. return sails.log.error(
  979. 'Invalid usage of publishCreate() :: ' +
  980. 'Values must have an `'+this.primaryKey+'`, instead got ::\n' +
  981. util.inspect(values)
  982. );
  983. }
  984. if (sails.util.isFunction(this.beforePublishCreate)) {
  985. this.beforePublishCreate(values, req);
  986. }
  987. var id = values[this.primaryKey];
  988. // If any of the added values were association attributes, publish add or remove messages.
  989. _.each(values, function(val, key) {
  990. // If the user hasn't yet given this association a value, bail out
  991. if (val === null) {
  992. return;
  993. }
  994. var association = _.find(this.associations, {alias: key});
  995. // If the attribute isn't an assoctiation, return
  996. if (!association) return;
  997. // Get the associated model class
  998. var ReferencedModel = sails.models[association.type == 'model' ? association.model : association.collection];
  999. // Bail if the model doesn't exist
  1000. if (!ReferencedModel) return;
  1001. // Bail if this attribute isn't in the model's schema
  1002. if (association.type == 'model') {
  1003. // Get the inverse association definition, if any
  1004. reverseAssociation = _.find(ReferencedModel.associations, {collection: this.identity, via: key}) || _.find(ReferencedModel.associations, {model: this.identity, via: key});
  1005. if (!reverseAssociation) {return;}
  1006. // If this is a to-many association, do publishAdd on the other side
  1007. // TODO -- support nested creates. For now, we can't tell if an object value here represents
  1008. // a NEW object or an existing one, so we'll ignore it.
  1009. if (reverseAssociation.type == 'collection' && !_.isObject(val)) {
  1010. ReferencedModel.publishAdd(val, reverseAssociation.alias, id, {noReverse:true});
  1011. }
  1012. // Otherwise do a publishUpdate
  1013. // TODO -- support nested creates. For now, we can't tell if an object value here represents
  1014. // a NEW object or an existing one, so we'll ignore it.
  1015. else {
  1016. var pubData = {};
  1017. if (!_.isObject(val)) {
  1018. pubData[reverseAssociation.alias] = id;
  1019. ReferencedModel.publishUpdate(val, pubData, req, {noReverse:true});
  1020. }
  1021. }
  1022. }
  1023. else {
  1024. // Get the inverse association definition, if any
  1025. reverseAssociation = _.find(ReferencedModel.associations, {collection: this.identity, via: key}) || _.find(ReferencedModel.associations, {model: this.identity, alias: association.via});
  1026. if (!reverseAssociation) {return;}
  1027. // If this is a to-many association, do publishAdds on the other side
  1028. if (reverseAssociation.type == 'collection') {
  1029. // Alert any added models
  1030. _.each(val, function(pk) {
  1031. // TODO -- support nested creates. For now, we can't tell if an object value here represents
  1032. // a NEW object or an existing one, so we'll ignore it.
  1033. if (_.isObject(pk)) return;
  1034. ReferencedModel.publishAdd(pk, reverseAssociation.alias, id, {noReverse:true});
  1035. });
  1036. }
  1037. // Otherwise do a publishUpdate
  1038. else {
  1039. // Alert any added models
  1040. _.each(val, function(pk) {
  1041. // TODO -- support nested creates. For now, we can't tell if an object value here represents
  1042. // a NEW object or an existing one, so we'll ignore it.
  1043. if (_.isObject(pk)) return;
  1044. var pubData = {};
  1045. pubData[reverseAssociation.alias] = id;
  1046. ReferencedModel.publishUpdate(pk, pubData, req, {noReverse:true});
  1047. });
  1048. }
  1049. }
  1050. }, this);
  1051. // Ensure that we're working with a plain object
  1052. values = _.clone(values);
  1053. // If a request object was sent, get its socket, otherwise assume a socket was sent.
  1054. var socketToOmit = (req && req.socket ? req.socket : req);
  1055. // Blast success message
  1056. sails.sockets.publishToFirehose({
  1057. model: this.identity,
  1058. verb: 'create',
  1059. data: values,
  1060. id: values[this.primaryKey]
  1061. });
  1062. // Publish to classroom
  1063. var eventName = this.identity;
  1064. this.broadcast(this._classRoom(), eventName, {
  1065. verb: 'created',
  1066. data: values,
  1067. id: values[this.primaryKey]
  1068. }, socketToOmit);
  1069. // Also broadcasts a message to the legacy class room (derived by
  1070. // using the `:legacy_v0.9` trailer on the class room name).
  1071. // Uses traditional eventName === "message".
  1072. // Uses traditional message format.
  1073. if (sails.config.sockets['backwardsCompatibilityFor0.9SocketClients']) {
  1074. var legacyData = _.cloneDeep({
  1075. verb: 'create',
  1076. data: values,
  1077. model: self.identity,
  1078. id: values[this.primaryKey]
  1079. });
  1080. var legacyRoom = this._classRoom()+':legacy_v0.9';
  1081. self.broadcast( legacyRoom, 'message', legacyData, socketToOmit );
  1082. }
  1083. // Subscribe watchers to the new instance
  1084. if (!options.noIntroduce) {
  1085. this.introduce(values[this.primaryKey]);
  1086. }
  1087. if (sails.util.isFunction(this.afterPublishCreate)) {
  1088. this.afterPublishCreate(values, req);
  1089. }
  1090. },
  1091. /**
  1092. *
  1093. * @return {[type]} [description]
  1094. */
  1095. watch: function ( req ) {
  1096. var socket = req.socket ? req.socket : req;
  1097. // todo: consider using the following instead (to lessen potential confusion w/ http requests)
  1098. // (however it would need to be applied to all occurances of `req` in methods in this hook)
  1099. // if (!socket.handshake) {
  1100. // sails.log.verbose('`Model.watch()` called by a non-socket request. Only requests originating from a connected socket may be subscribed to a Model class. Ignoring...');
  1101. // return;
  1102. // }
  1103. sails.sockets.join(socket, this._classRoom());
  1104. sails.log.silly("Subscribed socket ", sails.sockets.id(socket), "to", this._classRoom());
  1105. if (sails.config.sockets['backwardsCompatibilityFor0.9SocketClients'] && socket.handshake) {
  1106. var sdk = getSDKMetadata(socket.handshake);
  1107. var isLegacySocketClient = sdk.version === '0.9.0';
  1108. if (isLegacySocketClient) {
  1109. sails.sockets.join(socket, this._classRoom()+':legacy_v0.9');
  1110. }
  1111. }
  1112. },
  1113. /**
  1114. * [unwatch description]
  1115. * @param {[type]} socket [description]
  1116. * @return {[type]} [description]
  1117. */
  1118. unwatch: function ( req ) {
  1119. var socket = req.socket ? req.socket : req;
  1120. sails.sockets.leave(socket, this._classRoom());
  1121. sails.log.silly("Unubscribed socket ", sails.sockets.id(socket), "from", this._classRoom());
  1122. if (sails.config.sockets['backwardsCompatibilityFor0.9SocketClients'] && socket.handshake) {
  1123. var sdk = getSDKMetadata(socket.handshake);
  1124. var isLegacySocketClient = sdk.version === '0.9.0';
  1125. if (isLegacySocketClient) {
  1126. sails.sockets.leave(socket, this._classRoom()+':legacy_v0.9');
  1127. }
  1128. }
  1129. },
  1130. /**
  1131. * Introduce a new instance
  1132. *
  1133. * Take all of the subscribers to the class room and 'introduce' them
  1134. * to a new instance room
  1135. *
  1136. * @param {String|Finite} id
  1137. * - primary key of the instance we're referring to
  1138. *
  1139. * @api private
  1140. */
  1141. introduce: function(model) {
  1142. var id = model[this.primaryKey] || model;
  1143. _.each(this.watchers(), function(socketId) {
  1144. this.subscribe(socketId, id);
  1145. }, this);
  1146. },
  1147. /**
  1148. * Bid farewell to a destroyed instance
  1149. * Take all of the socket subscribers in this instance room
  1150. * and unsubscribe them from it
  1151. */
  1152. retire: function(model) {
  1153. var id = model[this.primaryKey] || model;
  1154. _.each(this.subscribers(id), function(socket) {
  1155. this.unsubscribe(socket, id);
  1156. }, this);
  1157. }
  1158. };
  1159. }
  1160. };