UNPKG

sails

Version:

API-driven framework for building realtime apps, using MVC conventions (based on Express and Socket.io)

1,148 lines (908 loc) 41.1 kB
/** * Module dependencies. */ var util = require('util'); var _ = require('@sailshq/lodash'); /** * Module errors */ var Err = { dependency: function (dependent, dependency) { return new Error( '\n' + 'Cannot use `' + dependent + '` hook ' + 'without the `' + dependency + '` hook enabled!' ); } }; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // TODO: Remove this hook altogether, instead splitting its contents between // the `blueprints` and `sockets` hooks. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** * pubsub hook * * > Implements public resourceful pubsub (RPS) methods, as well as some * > private methods used by the blueprints hook. */ module.exports = function(sails) { // Private function for parsing a potential instance ID. var parseId = function (id) { if(!_.isObject(this.attributes, this.primaryKey)) { return id; } var pkAttrDef = this.attributes[this.primaryKey]; if(_.isPlainObject(pkAttrDef)) { if (pkAttrDef.type === 'number') { return parseInt(id); } else if (pkAttrDef.type === 'string') { return new String(id).toString(); //jshint ignore:line } } return id; }; /** * Check that records are a list, if not, make them a list * Also if they are ids, make them dummy objects with an `id` property * * @param {Object|Array|String|Finite} records * @returns {Array} array of things that have an `id` property * * @api private * @synchronous */ var pluralize = function (records) { // If `records` is a non-array object, // turn it into a single-item array ("pluralize" it) // e.g. { id: 7 } -----> [ { id: 7 } ] if ( !_.isArray(records) ) { var record = records; records = [record]; } // If a list of ids things look ids (finite numbers or strings), // wrap them up as dummy objects; e.g. [1,2] ---> [ {id: 1}, {id: 2} ] var self = this; return _.map(records, function (record) { if ( _.isString(record) || _.isFinite(record) ) { var id = record; var data = {}; data[self.primaryKey] = id; return data; } if (_.isNull(record) || _.isUndefined(record)) { throw new Error('Could not coerce value into an array of records!'); } return record; }); }; /** * Expose Hook definition */ return { initialize: function(cb) { var self = this; // If `views` or `orm` hook is not enabled, complain and disable the hook. if (!sails.hooks.sockets || !sails.hooks.orm) { sails.log.verbose('Cannot use `pubsub` hook without the `sockets` and `orm` hooks enabled! (Skipping...)'); delete sails.hooks.pubsub; return cb(); } // If `views` or `orm` hook is not enabled, complain and respond w/ error if (!sails.hooks.sockets) { return cb( Err.dependency('pubsub', 'sockets') ); } if (!sails.hooks.orm) { return cb( Err.dependency('pubsub', 'orm') ); } // Wait for `hook:orm:loaded` sails.on('hook:orm:loaded', function() { // Do the heavy lifting self.augmentModels(); // Indicate that the hook is fully loaded cb(); }); // When the orm is reloaded, re-apply all of the pubsub methods to the // models sails.on('hook:orm:reloaded', function() { self.augmentModels(); // Trigger an event in case something needs to respond to the pubsub reload sails.emit('hook:pubsub:reloaded'); }); }, augmentModels: function() { // Augment models with room/socket logic (& bind context) for (var identity in sails.models) { var AugmentedModel = _.defaults(sails.models[identity], getPubsubMethods(), {autosubscribe: true} ); _.bindAll(AugmentedModel, 'subscribe', 'unsubscribe', 'publish', '_watch', '_room', '_introduce', '_retire', '_publishCreate', '_publishUpdate', '_publishDestroy', '_publishAdd', '_publishRemove' ); sails.models[identity] = AugmentedModel; } } }; /** * These methods get appended to the Model class objects * Some take req.socket as an argument to get access * to user('s|s') socket object(s) */ function getPubsubMethods () { return { // ██████╗ ██╗ ██╗██████╗ ██╗ ██╗ ██████╗ // ██╔══██╗██║ ██║██╔══██╗██║ ██║██╔════╝ // ██████╔╝██║ ██║██████╔╝██║ ██║██║ // ██╔═══╝ ██║ ██║██╔══██╗██║ ██║██║ // ██║ ╚██████╔╝██████╔╝███████╗██║╚██████╗ // ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝╚═╝ ╚═════╝ // /** * Broadcast a custom message to sockets connected to the specified records * @param {Object|String|Finite} records -- record or ID of record whose subscribers should receive the message * @param {Object|Array|String|Finite} data -- the message payload * @param {Request|Socket} req - if specified, broadcast using this * socket (effectively omitting it) * */ publish: function(ids, data, req) { var self = this; // ids is required. if (!ids) { sails.log.error('`' + self.identity + '.publish` : missing or empty second argument `ids`. API is `.publish(ids, data [, req])`.'); return; } // ids must be an array of primary keys -- we'll coerce it (with a warning) if it's not. if (!_.isArray(ids) || _.any(ids, function(id) {return !_.isString(id) && !_.isNumber(id);})) { sails.log.debug('The first argument passed to `' + self.identity + '.publish()` must be an array of ids. To subscribe to a single record, wrap the id in an array.'); try { ids = _.pluck(pluralize.apply(this, [ids]), this.primaryKey); } catch (err) { throw new Error('We tried to transform `' + util.inspect(ids, {depth: 2}) + '` into an array of IDs, but there was a problem (could some values have been `null` or `undefined`?) Details: '+err.stack); } sails.log.debug('For example: `[' + ids[0] + ']`'); sails.log.debug('Wrapping it in an array for you this time...'); } // If a request object was sent, get its socket, otherwise assume a socket was sent. var socketToOmit = (req && req.socket ? req.socket : req); // Ensure that we're working with a clean, unencumbered object data = _.cloneDeep(data); // Loop through the record IDs to broadcast to. _.each(ids, function(id) { var room = self._room(id); sails.sockets.broadcast( room, self.identity, data, socketToOmit ); }); }, /** * Subscribe a socket to a handful of records in this model * * Usage: * Model.subscribe(req, ids) * * @param {Request|Socket} req - request containing the socket to subscribe, or the socket itself * @param {Array} ids - array of ids of instances to subscribe to * * // Subscribe to User.update() and User.destroy() * // for the specified instances (or user.save() / user.destroy()): * User.subscribe(req.socket, users) * * @api public */ subscribe: function (req, ids) { var self = this; // If a request object was sent, get its socket, otherwise assume a socket was sent. var socket = sails.sockets.parseSocket(req); // Request must originate from a socket. if (!socket) { sails.log.debug('`Model.subscribe()` called by a non-socket request. Only requests originating from a connected socket may be subscribed. Ignoring...'); return; } if (!ids) { sails.log.error('`' + self.identity + '.subscribe` : missing or empty second argument `ids`. API is `.subscribe(request, ids)`.'); return; } if (!_.isArray(ids) || _.any(ids, function(id) {return !_.isString(id) && !_.isNumber(id);})) { sails.log.debug('The second argument passed to `' + self.identity + '.subscribe()` must be an array of ids. To subscribe to a single record, wrap the id in an array.'); try { ids = _.pluck(pluralize.apply(this, [ids]), this.primaryKey); } catch (err) { throw new Error('We tried to transform `' + util.inspect(ids, {depth: 2}) + '` into an array of IDs, but there was a problem (could some values have been `null` or `undefined`?) Details: '+err.stack); } sails.log.debug('For example: `[' + ids[0] + ']`'); sails.log.debug('Wrapping it in an array for you this time...'); } for (let id of ids) { // Attempt to join the room for the specified instance. sails.sockets.join( socket, self._room(id) ); sails.log.silly( 'Subscribed to the ' + self.globalId + ' with id=' + id + '\t(room :: ' + self._room(id) + ')' ); }//∞ }, /** * Unsubscribe a socket from some records * * Usage: * Model.unsubscribe(req, ids) * * @param {Request|Socket} req - request containing the socket to unsubscribe, or the socket itself * @param {Array} ids - array of ids of instances to unsubscribe from */ unsubscribe: function (req, ids) { var self = this; // If a request object was sent, get its socket, otherwise assume a socket was sent. var socket = sails.sockets.parseSocket(req); if (!socket) { sails.log.debug('`Model.unsubscribe()` called by a non-socket request. Only requests originating from a connected socket may be subscribed. Ignoring...'); return; } // If no ids provided, unsubscribe from the class room if (!ids) { sails.log.error('`' + self.identity + '.unsubscribe` : missing or empty second argument `ids`. API is `.subscribe(request, ids)`.'); return; } // ids must be an array of primary keys -- we'll coerce it (with a warning) if it's not. if (!_.isArray(ids) || _.any(ids, function(id) {return !_.isString(id) && !_.isNumber(id);})) { sails.log.debug('The second argument passed to `' + self.identity + '.unsubscribe()` must be an array of ids. To subscribe to a single record, wrap the id in an array.'); try { ids = _.pluck(pluralize.apply(this, [ids]), this.primaryKey); } catch (err) { throw new Error('We tried to transform `' + util.inspect(ids, {depth: 2}) + '` into an array of IDs, but there was a problem (could some values have been `null` or `undefined`?) Details: '+err.stack); } sails.log.debug('For example: `[' + ids[0] + ']`'); sails.log.debug('Wrapping it in an array for you this time...'); } for (let id of ids) { // Attempt to leave the room for the specified instance. sails.sockets.leave( socket, self._room(id)); sails.log.silly( 'Unsubscribed from the ' + self.globalId + ' with id=' + id + '\t(room :: ' + self._room(id) + ')' ); }//∞ }, /** * Get the socket room name for a model instance. * * Usage: * Model.getRoomName(id) * * @param {Number|String} id - the ID of the instance to get the room name for. */ getRoomName: function(id) { return this._room(id); }, // ██████╗ ██████╗ ██╗██╗ ██╗ █████╗ ████████╗███████╗ // ██╔══██╗██╔══██╗██║██║ ██║██╔══██╗╚══██╔══╝██╔════╝ // ██████╔╝██████╔╝██║██║ ██║███████║ ██║ █████╗ // ██╔═══╝ ██╔══██╗██║╚██╗ ██╔╝██╔══██║ ██║ ██╔══╝ // ██║ ██║ ██║██║ ╚████╔╝ ██║ ██║ ██║ ███████╗ // ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ // /** * Broadcast a resourceful pubsub message to sockets connected to the specified records * (or null to broadcast to the entire class room) * * @param {Object|Array|String|Finite} records -- records whose subscribers should receive the message * @param {Object|Array|String|Finite} data -- the message payload * socket (effectively omitting it) * * @api private */ _publishRPS: function (records, data, req) { var self = this; records = pluralize.apply(this, [records]); var ids = _.pluck(records, this.primaryKey); if ( ids.length === 0 ) { sails.log.debug('Can\'t publish a message to an empty list of instances-- ignoring...'); } // If a request object was sent, get its socket, otherwise assume a socket was sent. var socketToOmit = (req && req.socket ? req.socket : req); // Ensure that we're working with a clean, unencumbered object data = _.cloneDeep(data); // Loop through the record IDs to broadcast to. _.each(ids, function(id) { sails.sockets.broadcast( self._room(id), self.identity, data, socketToOmit ); }); }, /** * @param {String|Number} id Unique ID (i.e. primary key) of the record to get the room for * @param {String} name Name of the room to get the identifier for * @return {String} name of the instance room for an instance of this model w/ given id * @synchronous */ _room: function (id) { if (!id) { sails.log.error('Must specify an `id` when calling `Model._room(id)`'); return; } return 'sails_model_'+this.identity+'_'+id+':'+this.identity; }, /** * @return {String} name of this model's global class room * @synchronous * @api private */ _classRoom: function() { return 'sails_model_create_'+this.identity; }, /** * Publish an update on a particular model * * @param {String|Finite} id * - primary key of the instance we're referring to * * @param {Object} changes * - an object of changes to this instance that will be broadcasted * * @param {Request|Socket} req - if specified, broadcast using this socket (effectively omitting it) * * @api public */ _publishUpdate: function (id, changes, req, options) { var reverseAssociation; // Make sure there's an options object options = options || {}; // Ensure that we're working with a clean, unencumbered object changes = _.cloneDeep(changes); // Enforce valid usage var validId = _.isString(id) || _.isFinite(id); if ( !validId ) { return sails.log.error( 'Invalid usage of `' + this.identity + '._publishUpdate(id, changes, [socketToOmit])`' ); } if (_.isFunction(this._beforePublishUpdate)) { this._beforePublishUpdate(id, changes, req, options); } // Coerce id to match the attribute type of the primary key of the model id = parseId.apply(this,[id]); var data = { model: this.identity, verb: 'update', data: changes, id: id }; if (options.previous && !options.noReverse) { var previous = options.previous; // If any of the changes were to association attributes, publish add or remove messages. _.each(changes, function(val, key) { // If value wasn't changed, do nothing if (val === previous[key]) { return; } // Find an association matching this attribute var association = _.find(this.associations, {alias: key}); // If the attribute isn't an assoctiation, return if (!association) { return; } // Get the associated model class var ReferencedModel = sails.models[association.type === 'model' ? association.model : association.collection]; // Bail if this attribute isn't in the model's schema if (association.type === 'model') { var previousPK = _.isObject(previous[key]) ? previous[key][ReferencedModel.primaryKey] : previous[key]; var newPK = _.isObject(val) ? val[this.primaryKey] : val; if (previousPK === newPK) { return; } // Get the inverse association definition, if any reverseAssociation = _.find(ReferencedModel.associations, {collection: this.identity, via: key}) || _.find(ReferencedModel.associations, {model: this.identity, via: key}); if (!reverseAssociation) {return;} // If this is a to-many association, do _publishAdd or _publishRemove as necessary // on the other side if (reverseAssociation.type === 'collection') { // If there was a previous value, alert the previously associated model if (previous[key]) { ReferencedModel._publishRemove(previousPK, reverseAssociation.alias, id, req, {noReverse:true}); } // If there's a new value (i.e. it's not null), alert the newly associated model if (val) { ReferencedModel._publishAdd(newPK, reverseAssociation.alias, id, req, {noReverse:true}); } } // Otherwise do a _publishUpdate else { var pubData = {}; // If there was a previous association, notify it that it has been nullified if (previous[key]) { pubData[reverseAssociation.alias] = null; ReferencedModel._publishUpdate(previousPK, pubData, req, {noReverse:true}); } // If there's a new association, notify it that it has been linked if (val) { pubData[reverseAssociation.alias] = id; ReferencedModel._publishUpdate(newPK, pubData, req, {noReverse:true}); } } } else { // Get the reverse association definition, if any reverseAssociation = _.find(ReferencedModel.associations, {collection: this.identity, via: key}) || _.find(ReferencedModel.associations, {model: this.identity, alias: association.via}); if (!reverseAssociation) {return;} // If we can't get the previous PKs (b/c previous isn't populated), bail if (_.isUndefined(previous[key])) { return; } // Get the previous set of IDs var previousPKs = _.pluck(previous[key], ReferencedModel.primaryKey); // Get the current set of IDs var updatedPKs = _.map(val, function(_val) { if (_.isObject(_val)) { return _val[ReferencedModel.primaryKey]; } else { return _val; } }); // Find any values that were added to the collection var addedPKs = _.difference(updatedPKs, previousPKs); // Find any values that were removed from the collection var removedPKs = _.difference(previousPKs, updatedPKs); // If this is a to-many association, do _publishAdd or _publishRemove as necessary // on the other side if (reverseAssociation.type === 'collection') { // Alert any removed models _.each(removedPKs, function(pk) { ReferencedModel._publishRemove(pk, reverseAssociation.alias, id, req, {noReverse:true}); }); // Alert any added models _.each(addedPKs, function(pk) { ReferencedModel._publishAdd(pk, reverseAssociation.alias, id, req, {noReverse:true}); }); } // Otherwise do a _publishUpdate else { // Alert any removed models _.each(removedPKs, function(pk) { var pubData = {}; pubData[reverseAssociation.alias] = null; ReferencedModel._publishUpdate(pk, pubData, req, {noReverse:true}); }); // Alert any added models _.each(addedPKs, function(pk) { var pubData = {}; pubData[reverseAssociation.alias] = id; ReferencedModel._publishUpdate(pk, pubData, req, {noReverse:true}); }); }//</else> }//</else> }, this);//</_.each()> }//</ if `previous` and `!noReverse` > // If a request object was sent, get its socket, otherwise assume a socket was sent. var socketToOmit = (req && req.socket ? req.socket : req); data.verb = 'updated'; data.previous = options.previous; delete data.model; // Broadcast to the model instance room this._publishRPS(id, data, socketToOmit); if (_.isFunction(this._afterPublishUpdate)) { this._afterPublishUpdate(id, changes, req, options); } }, /** * Publish the destruction of a particular model * * @param {String|Finite} id * - primary key of the instance we're referring to * * @param {Request|Socket} req - if specified, broadcast using this socket (effectively omitting it) * */ _publishDestroy: function (id, req, options) { var reverseAssociation; options = options || {}; // Enforce valid usage var invalidId = !id || _.isObject(id); if ( invalidId ) { return sails.log.error( 'Invalid usage of `' + this.identity + '._publishDestroy(id, [socketToOmit])`' ); } if (_.isFunction(this._beforePublishDestroy)) { this._beforePublishDestroy(id, req, options); } // Coerce id to match the attribute type of the primary key of the model id = parseId.apply(this,[id]); var data = { model: this.identity, verb: 'destroy', id: id, previous: options.previous }; // If a request object was sent, get its socket, otherwise assume a socket was sent. var socketToOmit = (req && req.socket ? req.socket : req); data.verb = 'destroyed'; delete data.model; // Broadcast to the model instance room this._publishRPS(id, data, socketToOmit); // Unsubscribe everyone from the model instance this._retire(id); if (options.previous) { var previous = options.previous; // Loop through associations and alert as necessary _.each(this.associations, function(association) { var ReferencedModel; // If it's a to-one association, and it wasn't falsy, alert // the reverse side if (association.type === 'model' && association.alias && previous[association.alias]) { ReferencedModel = sails.models[association.model]; // Get the inverse association definition, if any reverseAssociation = _.find(ReferencedModel.associations, {collection: this.identity}) || _.find(ReferencedModel.associations, {model: this.identity}); if (reverseAssociation) { // If it's a to-one, publish a simple update alert var referencedModelId = _.isObject(previous[association.alias]) ? previous[association.alias][ReferencedModel.primaryKey] : previous[association.alias]; if (reverseAssociation.type === 'model') { var pubData = {}; pubData[reverseAssociation.alias] = null; ReferencedModel._publishUpdate(referencedModelId, pubData, req, {noReverse:true}); } // If it's a to-many, publish a "removed" alert else { ReferencedModel._publishRemove(referencedModelId, reverseAssociation.alias, id, req, {noReverse:true}); } } } else if (association.type === 'collection' && association.via && previous[association.alias] && previous[association.alias].length) { ReferencedModel = sails.models[association.collection]; // Get the inverse association definition, if any var reverseAttribute = ReferencedModel.attributes[association.via]; _.each(previous[association.alias], function(associatedModel) { // If it's a to-one, publish a simple update alert if (reverseAttribute.model) { var pubData = {}; pubData[association.via] = null; ReferencedModel._publishUpdate(associatedModel[ReferencedModel.primaryKey], pubData, req, {noReverse:true}); } // If it's a to-many, publish a "removed" alert else { ReferencedModel._publishRemove(associatedModel[ReferencedModel.primaryKey], association.via, id, req, {noReverse:true}); } }); } }, this); } if (_.isFunction(this._afterPublishDestroy)) { this._afterPublishDestroy(id, req, options); } }, /** * _publishAdd * * @param {[type]} id [description] * @param {[type]} alias [description] * @param {[type]} idAdded [description] * @param {[type]} socketToOmit [description] */ _publishAdd: function(id, alias, added, req, options) { var reverseAssociation; // Make sure there's an options object options = options || {}; // Enforce valid usage var invalidId = !id || _.isObject(id); var invalidAlias = !alias || !_.isString(alias); var invalidAddedId = !added || _.isArray(added); if ( invalidId || invalidAlias || invalidAddedId ) { return sails.log.error( 'Invalid usage of `' + this.identity + '._publishAdd(id, alias, idAdded|recordAdded, [socketToOmit])`' ); } // Get the model on the opposite side of the association var reverseModel = sails.models[_.find(this.associations, {alias: alias}).collection]; // Determine whether `added` was provided as a pk value or an object var idAdded; // If it is a pk value, we'll turn it into `idAdded`: if (!_.isObject(added)) { idAdded = added; added = undefined; } // Otherwise we'll leave it as `added` for use below, and determine `idAdded` by examining the object // using our knowledge of what the name of the primary key attribute is. else { idAdded = added[reverseModel.primaryKey]; // If we don't find a primary key value, we'll log an error and return early. if (!_.isString(idAdded) && !_.isNumber(idAdded)) { sails.log.error( 'Invalid usage of _publishAdd(): expected object provided '+ 'for `recordAdded` to have a `%s` attribute', reverseModel.primaryKey ); return; } } // Coerce id to match the attribute type of the primary key of the model id = parseId.apply(this,[id]); // Coerce idAdded to match the attribute type of the primary key of the reverse model idAdded = parseId.apply(reverseModel,[idAdded]); // Lifecycle event if (_.isFunction(this._beforePublishAdd)) { this._beforePublishAdd(id, alias, idAdded, req); } // If a request object was sent, get its socket, otherwise assume a socket was sent. var socketToOmit = (req && req.socket ? req.socket : req); this._publishRPS(id, (function (){ var event = { id: id, verb: 'addedTo', attribute: alias, addedId: idAdded }; if (added) { event.added = added; } return event; })(), socketToOmit); if (!options.noReverse) { var data; // Subscribe to the model you're adding if (req && req.isSocket) { data = {}; data[reverseModel.primaryKey] = idAdded; reverseModel.subscribe(req, [idAdded]); } // Find the reverse association, if any reverseAssociation = _.find(reverseModel.associations, {alias: _.find(this.associations, {alias: alias}).via}); if (reverseAssociation) { // If this is a many-to-many association, do a _publishAdd for the // other side. if (reverseAssociation.type === 'collection') { reverseModel._publishAdd(idAdded, reverseAssociation.alias, id, req, {noReverse:true}); } // Otherwise, do a _publishUpdate else { data = {}; data[reverseAssociation.alias] = id; reverseModel._publishUpdate(idAdded, data, req, {noReverse:true}); } } } if (_.isFunction(this._afterPublishAdd)) { this._afterPublishAdd(id, alias, idAdded, req); } }, /** * _publishRemove * * @param {[type]} id [description] * @param {[type]} alias [description] * @param {[type]} idRemoved [description] * @param {[type]} socketToOmit [description] */ _publishRemove: function(id, alias, idRemoved, req, options) { var reverseAssociation; // Make sure there's an options object options = options || {}; // Enforce valid usage var invalidId = !id || _.isObject(id); var invalidAlias = !alias || !_.isString(alias); var invalidRemovedId = !idRemoved || _.isObject(idRemoved); if ( invalidId || invalidAlias || invalidRemovedId ) { return sails.log.error( 'Invalid usage of `' + this.identity + '._publishRemove(id, alias, idRemoved, [socketToOmit])`' ); } if (_.isFunction(this._beforePublishRemove)) { this._beforePublishRemove(id, alias, idRemoved, req); } // Get the reverse model. var reverseModel = sails.models[_.find(this.associations, {alias: alias}).collection]; // Coerce id to match the attribute type of the primary key of the model id = parseId.apply(this,[id]); // Coerce idRemoved to match the attribute type of the primary key of the reverse model idRemoved = parseId.apply(reverseModel,[idRemoved]); // If a request object was sent, get its socket, otherwise assume a socket was sent. var socketToOmit = (req && req.socket ? req.socket : req); this._publishRPS(id, { id: id, verb: 'removedFrom', attribute: alias, removedId: idRemoved }, socketToOmit); if (!options.noReverse) { // Get the reverse association, if any. reverseAssociation = _.find(reverseModel.associations, {alias: _.find(this.associations, {alias: alias}).via}); if (reverseAssociation) { // If this is a many-to-many association, do a _publishAdd for the // other side. if (reverseAssociation.type === 'collection') { reverseModel._publishRemove(idRemoved, reverseAssociation.alias, id, req, {noReverse:true}); } // Otherwise, do a _publishUpdate else { var data = {}; data[reverseAssociation.alias] = null; reverseModel._publishUpdate(idRemoved, data, req, {noReverse:true}); } } } if (_.isFunction(this._afterPublishRemove)) { this._afterPublishRemove(id, alias, idRemoved, req); } }, /** * Publish the creation of model or an array of models * * @param {[Object]|Object} models * - the data to publish * * @param {Request|Socket} req - Optional request for broadcast. * @api private */ _publishCreate: function(models, req, options){ var self = this; // Pluralize so we can use this method regardless of it is an array or not models = pluralize.apply(this, [models]); //Publish all models _.each(models, function(values){ self._publishCreateSingle(values, req, options); }); }, /** * Publish the creation of a model * * @param {Object} values * - the data to publish * * @param {Request|Socket} req - if specified, broadcast using this socket (effectively omitting it) * @api private */ _publishCreateSingle: function(values, req, options) { var reverseAssociation; options = options || {}; if (_.isUndefined(values[this.primaryKey])) { return sails.log.error( 'Invalid usage of _publishCreate() :: ' + 'Values must have an `'+this.primaryKey+'`, instead got ::\n' + util.inspect(values) ); } if (_.isFunction(this._beforePublishCreate)) { this._beforePublishCreate(values, req); } var id = values[this.primaryKey]; // Coerce id to match the attribute type of the primary key of the model id = parseId.apply(this,[id]); // If any of the added values were association attributes, publish add or remove messages. _.each(values, function(val, key) { // If the user hasn't yet given this association a value, bail out if (val === null) { return; } var association = _.find(this.associations, {alias: key}); // If the attribute isn't an assoctiation, return if (!association) { return; } // Get the associated model class var ReferencedModel = sails.models[association.type === 'model' ? association.model : association.collection]; // Bail if the model doesn't exist if (!ReferencedModel) { return; } // Bail if this attribute isn't in the model's schema if (association.type === 'model') { // Get the inverse association definition, if any reverseAssociation = _.find(ReferencedModel.associations, {collection: this.identity, via: key}) || _.find(ReferencedModel.associations, {model: this.identity, via: key}); if (!reverseAssociation) {return;} // If this is a to-many association, do _publishAdd on the other side if (reverseAssociation.type === 'collection') { ReferencedModel._publishAdd( // Depending on the `populate` setting, the val could be an object or a primary key, // so we'll allow for both. val[ReferencedModel.primaryKey] || val, reverseAssociation.alias, id, req, {noReverse:true} ); } } else { // Get the inverse association definition, if any reverseAssociation = _.find(ReferencedModel.associations, {collection: this.identity, via: key}) || _.find(ReferencedModel.associations, {model: this.identity, alias: association.via}); if (!reverseAssociation) {return;} // If this is a to-many association, do publishAdds on the other side if (reverseAssociation.type === 'collection') { // Alert any added models _.each(val, function(pk) { // Depending on the `populate` setting, the val could be an object or a primary key, // so we'll allow for both. if (_.isObject(pk)) { pk = pk[ReferencedModel.primaryKey]; } ReferencedModel._publishAdd(pk, reverseAssociation.alias, id, req, {noReverse:true}); }); } // Otherwise do a _publishUpdate else { // Alert any added models _.each(val, function(pk) { // Depending on the `populate` setting, the val could be an object or a primary key, // so we'll allow for both. if (_.isObject(pk)) { pk = pk[ReferencedModel.primaryKey]; } var pubData = {}; pubData[reverseAssociation.alias] = id; ReferencedModel._publishUpdate(pk, pubData, req, {noReverse:true}); }); } } }, this); // Ensure that we're working with a plain object values = _.clone(values); // If a request object was sent, get its socket, otherwise assume a socket was sent. var socketToOmit = (req && req.socket ? req.socket : req); // Publish to classroom var payload = { verb: 'created', data: values, id: values[this.primaryKey] }; sails.log.silly('Published message to ', this._classRoom(), ': ', payload); var eventName = this.identity; sails.sockets.broadcast(this._classRoom(), eventName, payload, socketToOmit); // Subscribe watchers to the new instance if (!options.noIntroduce) { this._introduce(values[this.primaryKey]); } if (_.isFunction(this._afterPublishCreate)) { this._afterPublishCreate(values, req); } }, /** * * @return {[type]} [description] */ _watch: function ( req ) { var socket = sails.sockets.parseSocket(req); if (!socket) { sails.log.debug('`Model._watch()` called by a non-socket request. Only requests originating from a connected socket may be subscribed. Ignoring...'); return; }//-• sails.sockets.join(socket, this._classRoom()); sails.log.silly('Subscribed socket ', sails.sockets.getId(socket), 'to', this._classRoom()); }, /** * Introduce a new instance * * Take all of the subscribers to the class room and 'introduce' them * to a new instance room * * @param {String|Finite} id * - primary key of the instance we're referring to * * @api private */ _introduce: function(model) { var self = this; // Get the instance ID var id = model[this.primaryKey] || model; // Use addRoomMembersToRooms to subscribe everyone in the class room to the model identity instance room sails.sockets.addRoomMembersToRooms(self._classRoom(), self._room(id) ); }, /** * Bid farewell to a destroyed instance * Take all of the socket subscribers in this instance room * and unsubscribe them from it */ _retire: function(model) { var self = this; // Get the instance ID var id = model[this.primaryKey] || model; // Use removeRoomMembersFromRooms to unsubscribe everyone in the class room from the model identity instance room sails.sockets.removeRoomMembersFromRooms(self._classRoom(), self._room(id) ); } }; } };