├── .gitignore ├── Procfile ├── README.md ├── app.js ├── bin └── www ├── config.js ├── package.json ├── public └── stylesheets │ └── style.css ├── routes └── index.js └── views ├── error.hbs ├── index.hbs └── layout.hbs /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | my_config.js 3 | npm-debug.log 4 | node_modules/ 5 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node ./bin/www 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | node-streaming-socketio 2 | ======================= 3 | 4 | Node demo that streams newly created and updated Account records in Salesforce.com to the browser using socket.io. You can run the demo by opening two browsers: 5 | 6 | 1. [salesforce CRUD demo with Node](http://node-nforce-demo.herokuapp.com) - Create a new Account record in this app. 7 | 2. [node-streaming-socketio demo](http://node-streaming-socketio.herokuapp.com/) - New Account record are streamed to this app and display in the browser using socket.io. 8 | 9 | ### Setup Remote Access in Salesforce.com 10 | 11 | Setup a new Remote Access to get your OAuth tokens. If you are unfamiliar with settng this up, see 4:45 of my [Salesforce.com Primer for New Developers](http://www.youtube.com/watch?v=fq2ju2ML9GM). For your callback, simply use: http://localhost:3001/oauth/_callback 12 | 13 | ### Create a PushTopic in Salesforce.com 14 | 15 | Create a new PushTopic from the Developer Console in your org with the following. This will setup the endpoint for nforce to listen to: 16 | 17 | ``` 18 | PushTopic pt = new PushTopic(); 19 | pt.apiversion = 24.0; 20 | pt.name = 'AllAccounts'; 21 | pt.description = 'All new account records'; 22 | pt.query = 'SELECT Id, Name FROM Account'; 23 | insert pt; 24 | System.debug('Created new PushTopic: '+ pt.Id); 25 | ``` 26 | 27 | You can also set up PushTopics using the [Workbench](https://workbench.developerforce.com). 28 | 29 | ### Running the Application Locally 30 | 31 | ``` 32 | git clone https://github.com/jeffdonthemic/node-streaming-socketio.git 33 | cd node-streaming-socketio 34 | npm install 35 | ``` 36 | 37 | This will clone this repo locally so you simply have to make your config changes and be up and running. Now replace your OAuth tokens and credentials in the config.js file then run the following to start the server: 38 | 39 | ``` 40 | node app.js 41 | ``` 42 | 43 | Open your browser to [http://localhost:3001](http://localhost:3001) and make a change to an Account record in Salesforce and see it appear on the page. 44 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var path = require('path'); 3 | var favicon = require('serve-favicon'); 4 | var logger = require('morgan'); 5 | var cookieParser = require('cookie-parser'); 6 | var bodyParser = require('body-parser'); 7 | var config = require('./config.js'); 8 | var nforce = require('nforce'); 9 | 10 | var routes = require('./routes/index'); 11 | 12 | var app = express(); 13 | var server = require('http').Server(app); 14 | var io = require('socket.io')(server); 15 | // get a reference to the socket once a client connects 16 | var socket = io.sockets.on('connection', function (socket) { }); 17 | 18 | var org = nforce.createConnection({ 19 | clientId: config.CLIENT_ID, 20 | clientSecret: config.CLIENT_SECRET, 21 | redirectUri: config.CALLBACK_URL + '/oauth/_callback', 22 | mode: 'multi', 23 | environment: config.ENVIRONMENT // optional, sandbox or production, production default 24 | }); 25 | 26 | org.authenticate({ username: config.USERNAME, password: config.PASSWORD }, function(err, oauth) { 27 | 28 | if(err) return console.log(err); 29 | if(!err) { 30 | console.log('*** Successfully connected to Salesforce ***'); 31 | // add any logic to perform after login 32 | } 33 | 34 | // subscribe to a pushtopic 35 | var str = org.stream({ topic: config.PUSH_TOPIC, oauth: oauth }); 36 | 37 | str.on('connect', function(){ 38 | console.log('Connected to pushtopic: ' + config.PUSH_TOPIC); 39 | }); 40 | 41 | str.on('error', function(error) { 42 | console.log('Error received from pushtopic: ' + error); 43 | }); 44 | 45 | str.on('data', function(data) { 46 | console.log('Received the following from pushtopic ---'); 47 | console.log(data); 48 | // emit the record to be displayed on the page 49 | socket.emit('record-processed', JSON.stringify(data)); 50 | }); 51 | 52 | }); 53 | 54 | // view engine setup 55 | app.set('views', path.join(__dirname, 'views')); 56 | app.set('view engine', 'hbs'); 57 | 58 | app.use(function(req, res, next){ 59 | res.io = io; 60 | next(); 61 | }); 62 | 63 | // uncomment after placing your favicon in /public 64 | //app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); 65 | app.use(logger('dev')); 66 | app.use(bodyParser.json()); 67 | app.use(bodyParser.urlencoded({ extended: false })); 68 | app.use(cookieParser()); 69 | app.use(express.static(path.join(__dirname, 'public'))); 70 | 71 | app.use('/', routes); 72 | 73 | // catch 404 and forward to error handler 74 | app.use(function(req, res, next) { 75 | var err = new Error('Not Found'); 76 | err.status = 404; 77 | next(err); 78 | }); 79 | 80 | // error handlers 81 | 82 | // development error handler 83 | // will print stacktrace 84 | if (app.get('env') === 'development') { 85 | app.use(function(err, req, res, next) { 86 | res.status(err.status || 500); 87 | res.render('error', { 88 | message: err.message, 89 | error: err 90 | }); 91 | }); 92 | } 93 | 94 | // production error handler 95 | // no stacktraces leaked to user 96 | app.use(function(err, req, res, next) { 97 | res.status(err.status || 500); 98 | res.render('error', { 99 | message: err.message, 100 | error: {} 101 | }); 102 | }); 103 | 104 | 105 | module.exports = {app: app, server: server}; 106 | -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require('../app').app; 8 | var debug = require('debug')('node-streaming-socketio:server'); 9 | var http = require('http'); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | var port = normalizePort(process.env.PORT || '3001'); 16 | app.set('port', port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | var server = require('../app').server; 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | 28 | server.listen(port); 29 | server.on('error', onError); 30 | server.on('listening', onListening); 31 | 32 | /** 33 | * Normalize a port into a number, string, or false. 34 | */ 35 | 36 | function normalizePort(val) { 37 | var port = parseInt(val, 10); 38 | 39 | if (isNaN(port)) { 40 | // named pipe 41 | return val; 42 | } 43 | 44 | if (port >= 0) { 45 | // port number 46 | return port; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | /** 53 | * Event listener for HTTP server "error" event. 54 | */ 55 | 56 | function onError(error) { 57 | if (error.syscall !== 'listen') { 58 | throw error; 59 | } 60 | 61 | var bind = typeof port === 'string' 62 | ? 'Pipe ' + port 63 | : 'Port ' + port; 64 | 65 | // handle specific listen errors with friendly messages 66 | switch (error.code) { 67 | case 'EACCES': 68 | console.error(bind + ' requires elevated privileges'); 69 | process.exit(1); 70 | break; 71 | case 'EADDRINUSE': 72 | console.error(bind + ' is already in use'); 73 | process.exit(1); 74 | break; 75 | default: 76 | throw error; 77 | } 78 | } 79 | 80 | /** 81 | * Event listener for HTTP server "listening" event. 82 | */ 83 | 84 | function onListening() { 85 | var addr = server.address(); 86 | var bind = typeof addr === 'string' 87 | ? 'pipe ' + addr 88 | : 'port ' + addr.port; 89 | debug('Listening on ' + bind); 90 | } 91 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | exports.PORT = process.env.PORT || 3001; // use heroku's dynamic port or 3001 if localhost 2 | exports.DEBUG = true; 3 | exports.ENVIRONMENT = 'production'; 4 | exports.CALLBACK_URL = 'http://localhost:3001'; 5 | exports.PUSH_TOPIC = 'AllAccounts'; 6 | 7 | exports.CLIENT_ID = process.env.CLIENT_ID || "YOUR-CLIENT-ID"; 8 | exports.CLIENT_SECRET = process.env.CLIENT_SECRET || "YOUR-CLIENT-SECRET"; 9 | exports.USERNAME = process.env.USERNAME || "YOUR-USERNAME"; 10 | exports.PASSWORD = process.env.PASSWORD || "YOUR-PASSWORD"; 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-streaming-socketio", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node ./bin/www" 7 | }, 8 | "dependencies": { 9 | "body-parser": "~1.13.2", 10 | "cookie-parser": "~1.3.5", 11 | "debug": "~2.2.0", 12 | "express": "~4.13.1", 13 | "hbs": "~3.1.0", 14 | "morgan": "~1.6.1", 15 | "nforce": "^1.5.0", 16 | "serve-favicon": "~2.3.0", 17 | "socket.io": "^1.4.8" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00B7FF; 8 | } 9 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | 4 | /* GET home page. */ 5 | router.get('/', function(req, res, next) { 6 | res.render('index', { title: 'Force.com Streaming API Demo with nforce' }); 7 | }); 8 | 9 | module.exports = router; 10 | -------------------------------------------------------------------------------- /views/error.hbs: -------------------------------------------------------------------------------- 1 |

{{message}}

2 |

{{error.status}}

3 |
{{error.stack}}
4 | -------------------------------------------------------------------------------- /views/index.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ title }} 5 | 6 | 7 | 8 | 17 | 18 | 19 |

{{ title }}

20 |

Waiting for messages to magically appear below. Go update an account to watch the magic!

21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /views/layout.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{title}} 5 | 6 | 7 | 8 | {{{body}}} 9 | 10 | 11 | --------------------------------------------------------------------------------