├── Procfile ├── .DS_Store ├── views ├── index.jade ├── layout.jade ├── accounts.jade ├── show.jade ├── edit.jade └── new.jade ├── public └── stylesheets │ └── style.css ├── package.json ├── routes └── index.js ├── README.md ├── oauth.js ├── app.js └── rest.js /Procfile: -------------------------------------------------------------------------------- 1 | web: node app.js -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffdonthemic/Node-Force.com-REST-Demo/HEAD/.DS_Store -------------------------------------------------------------------------------- /views/index.jade: -------------------------------------------------------------------------------- 1 | h1= title 2 | p Welcome 3 | ul 4 | li: a(href='/accounts') List of 10 Accounts -------------------------------------------------------------------------------- /views/layout.jade: -------------------------------------------------------------------------------- 1 | !!! 2 | html 3 | head 4 | title= title 5 | link(rel='stylesheet', href='/stylesheets/style.css') 6 | body!= body -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /views/accounts.jade: -------------------------------------------------------------------------------- 1 | h1= title 2 | a(href='/accounts/new') Create a new Account 3 | p Existing Accounts 4 | ul 5 | each item in data['records'] 6 | li: a(href='/accounts/#{item['Id']}') #{item['Name']} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "application-name" 3 | , "version": "0.0.1" 4 | , "private": true 5 | , "dependencies": { 6 | "express": "2.5.8" 7 | , "jade": ">= 0.0.1", 8 | "restler": "0.2.1" 9 | } 10 | } -------------------------------------------------------------------------------- /views/show.jade: -------------------------------------------------------------------------------- 1 | // fields NOT to be displayed 2 | - var skipFields = ['attributes','IsDeleted']; 3 | h1= title 4 | a(href='/accounts/#{data['Id']}/edit') Edit this record 5 | ul 6 | each val, key in data 7 | - if (skipFields.indexOf(key) == -1) 8 | li #{key}: #{val} -------------------------------------------------------------------------------- /views/edit.jade: -------------------------------------------------------------------------------- 1 | // form fields to be displayed 2 | - var formFields = ['Name','BillingCity']; 3 | h1= title 4 | form(method='post', action='/accounts/#{data['Id']}/update') 5 | each val, key in data 6 | - if (formFields.indexOf(key) != -1) 7 | div 8 | label #{key}: 9 | input(name='account[#{key}]', value=val || '', type='text') 10 | div 11 | input(type='submit', value='Save') -------------------------------------------------------------------------------- /views/new.jade: -------------------------------------------------------------------------------- 1 | // generate a form based upon metadata & display the following field 2 | - var formFields = ['Name','BillingCity']; 3 | h1= title 4 | form(method='post', action='/accounts/create') 5 | each field in data.fields 6 | - if (formFields.indexOf(field.name) != -1) 7 | div 8 | label #{field.label}: 9 | input(name='account[#{field.name}]', value='', type='text') 10 | div 11 | input(type='submit', value='Save') -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | // only needed if calling the rest api from this file (accounts route) 4 | var rest = rest = require('./../rest.js'); 5 | 6 | /* 7 | * GET home page. 8 | */ 9 | exports.index = function(req, res){ 10 | res.render('index', { title: 'Salesforce.com Node.js REST Demo' }) 11 | }; 12 | 13 | /* 14 | * GET list of accounts - for larger apps, you may want to separate 15 | * code into different routes for ease of maintenance and logic. 16 | * Prevents app.js from becoming huge! 17 | */ 18 | exports.accounts = function(req, res){ 19 | rest.api(req).query("select id, name from account limit 10", function(data) { 20 | res.render("accounts", { title: 'Accounts', data: data, user: req.session.identity } ); 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Simple Node.js sample application using the salesforce.com REST API and OAuth for CRUDing accounts. 2 | 3 | You can run the application for yourself at the following url. You can authorize access to a production or developer org (recommended) to test out the functionality. 4 | 5 | Demo app at [https://node-sfdc-demo.herokuapp.com](https://node-sfdc-demo.herokuapp.com) & [blog post](http://bit.ly/IwbMJV) 6 | 7 | Localhost Setup 8 | =============== 9 | 10 | The first thing you need to do is set up Remote Access for your application running locally. Log into your DE org and go to Setup -> App Setup -> Develop -> Remote Access 11 | 12 | Create a new Remote Access and use the callback http://localhost:3001 (or whatever port you config the app for). Copy the values for the consumer key and consumer secret into app.js. 13 | 14 | If you don't have node.js installed, you can do so from [http://nodejs.org/#download](http://nodejs.org/#download) 15 | 16 | You'll also need to install [Express](http://expressjs.com). The guide and quick start are available at [http://expressjs.com/guide.html](http://expressjs.com/guide.html) but you can install it globally with: 17 | 18 | npm install -g express 19 | 20 | You'll also need to install [restler](https://github.com/danwrong/restler), a REST client library for node.js: 21 | 22 | npm install restler 23 | 24 | I would also highly recommend you install [node-dev](https://github.com/fgnass/node-dev)!!! Node-dev is a development tool for Node.js that automatically restarts the node process when a script is modified. With node-dev you don't have to hit CTRL+C Up-Arrow Enter after every change to your Node.js application. 25 | 26 | npm install -g node-dev 27 | 28 | Now you can start your app with: 29 | 30 | node-dev app.js 31 | 32 | Heroku Setup 33 | ============ 34 | 35 | Create a new application on heroku: 36 | 37 | heroku create [YOUR-APP-NAME] --stack cedar 38 | 39 | Add it to git and then push it to heroku. 40 | 41 | To run the application on heroku, you'll need set up another Remote Access that points to your heroku site. Salesforce.com requires that non-localhost applications use SSL. Fortunately heroku makes it extremely easy to add SSL. The piggyback SSL add-on is now a platform feature and available by default to all Heroku applications. No need adding the add-on any more!! Just setup another Remote Access with the following callback url: 42 | 43 | https://[YOUR-APP-NAME].herokuapp.com/token 44 | 45 | The last thing you should need to do is add your new environment variables to heroku with the following: 46 | 47 | 1. heroku config:add CLIENT_ID=YOUR-REMOTE-ACCESS-CONSUMER-KEY 48 | 2. heroku config:add CLIENT_SECRET=YOUR-REMOTE-ACCESS-CONSUMER-SECRET 49 | 3. heroku config:add LOGIN_SERVER=https://login.salesforce.com 50 | 4. heroku config:add REDIRECT_URI=https://[YOUR-APP-NAME].herokuapp.com/token 51 | 52 | You can confirm your environment variables for your app with: 53 | 54 | heroku config 55 | 56 | Access your application running on heroku and start the OAuth dance! 57 | 58 | https://[YOUR-APP-NAME].herokuapp.com -------------------------------------------------------------------------------- /oauth.js: -------------------------------------------------------------------------------- 1 | var rest = require('restler'); 2 | 3 | exports.refresh = function refresh(options) { 4 | console.log('refresh'); 5 | rest.post(options.oauth.loginServer+'/services/oauth2/token', { 6 | data: { 7 | grant_type: 'refresh_token', 8 | client_id: options.oauth.clientId, 9 | client_secret: options.oauth.clientSecret, 10 | refresh_token: options.oauth.refresh_token 11 | }, 12 | }).on('complete', function(data, response) { 13 | if (response.statusCode == 200) { 14 | console.log('refreshed: '+data.access_token); 15 | options.callback(data); 16 | } 17 | }).on('error', function(e) { 18 | console.error(e); 19 | }); 20 | } 21 | 22 | exports.oauth = function oauth(options) { 23 | var loginServer = options.loginServer || 'https://login.salesforce.com/', 24 | clientId = options.clientId, 25 | clientSecret = options.clientSecret, 26 | redirectUri = options.redirectUri; 27 | 28 | return function oauth(req, res, next){ 29 | console.log('oauth'); 30 | console.log('url :'+req.url); 31 | if (req.session && req.session.oauth) { 32 | // We're done - decorate the request with the oauth object 33 | req.oauth = req.session.oauth; 34 | req.oauth.loginServer = loginServer; 35 | req.oauth.clientId = clientId; 36 | req.oauth.clientSecret = clientSecret; 37 | console.log(req.session.oauth); 38 | next(); 39 | } else if (req.query.code){ 40 | // Callback from the Authorization Server 41 | console.log('code: '+req.query.code); 42 | 43 | rest.post(loginServer+'/services/oauth2/token', { 44 | data: { 45 | code: req.query.code, 46 | grant_type: 'authorization_code', 47 | client_id: clientId, 48 | redirect_uri: redirectUri, 49 | client_secret: clientSecret 50 | }, 51 | }).on('complete', function(data, response) { 52 | if (response.statusCode == 200) { 53 | req.session.oauth = data; 54 | state = req.session.oauth_state; 55 | delete req.session.oauth_state; 56 | console.log('oauth done - redirecting to '+state); 57 | res.redirect(state); 58 | } 59 | }).on('error', function(e) { 60 | console.error(e); 61 | }); 62 | } else { 63 | // Test for req.session - browser requests favicon.ico but doesn't 64 | // bother sending cookie? 65 | if ( req.session ) { 66 | // We have nothing - redirect to the Authorization Server 67 | req.session.oauth_state = req.url; 68 | var oauthURL = loginServer + "/services/oauth2/authorize?response_type=code&" + 69 | "client_id=" + clientId + "&redirect_uri=" + redirectUri + "&display=touch"; 70 | console.log('redirecting: '+oauthURL); 71 | res.redirect(oauthURL); // Redirect to salesforce.com 72 | res.end(); 73 | } else { 74 | next(); 75 | } 76 | } 77 | }; 78 | }; 79 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | var express = require('express') 6 | , routes = require('./routes') 7 | , rest = require('./rest.js') 8 | , oauth = require('./oauth.js') 9 | , url = require('url'); 10 | 11 | /** 12 | * Setup some environment variables (heroku) with defaults if not present 13 | */ 14 | var port = process.env.PORT || 3001; // use heroku's dynamic port or 3001 if localhost 15 | var cid = process.env.CLIENT_ID || "YOUR-REMOTE-ACCESS-CONSUMER-KEY"; 16 | var csecr = process.env.CLIENT_SECRET || "YOUR-REMOTE-ACCESS-CONSUMER-SECRET"; 17 | var lserv = process.env.LOGIN_SERVER || "https://login.salesforce.com"; 18 | var redir = process.env.REDIRECT_URI || "http://localhost:" + port + "/token"; 19 | 20 | /** 21 | * Middleware to call identity service and attach result to session 22 | */ 23 | function idcheck() { 24 | return function(req, res, next) { 25 | // Invoke identity service if we haven't got one or access token has 26 | // changed since we got it 27 | if (!req.session || !req.session.identity || req.session.identity_check != req.oauth.access_token) { 28 | rest.api(req).identity(function(data) { 29 | console.log(data); 30 | req.session.identity = data; 31 | req.session.identity_check = req.oauth.access_token; 32 | next(); 33 | }); 34 | } else { 35 | next(); 36 | } 37 | } 38 | } 39 | 40 | /** 41 | * Create the server 42 | */ 43 | var app = express.createServer( 44 | express.cookieParser(), 45 | express.session({ secret: csecr }), 46 | express.query(), 47 | oauth.oauth({ 48 | clientId: cid, 49 | clientSecret: csecr, 50 | loginServer: lserv, 51 | redirectUri: redir, 52 | }), 53 | idcheck() 54 | ); 55 | 56 | /** 57 | * Configuration the server 58 | */ 59 | app.configure(function(){ 60 | app.set('views', __dirname + '/views'); 61 | app.set('view engine', 'jade'); 62 | app.use(express.bodyParser()); 63 | app.use(express.methodOverride()); 64 | app.use(app.router); 65 | app.use(express.static(__dirname + '/public')); 66 | }); 67 | 68 | app.configure('development', function(){ 69 | app.use(express.errorHandler({ dumpExceptions: true, showStack: true })); 70 | }); 71 | 72 | app.configure('production', function(){ 73 | app.use(express.errorHandler()); 74 | }); 75 | 76 | /** 77 | * Routes 78 | */ 79 | 80 | // 'home' page 81 | app.get('/', routes.index); 82 | 83 | // list of accounts - see routes/index.js for more info 84 | app.get('/accounts', routes.accounts); 85 | 86 | // form to create a new account 87 | app.get('/accounts/new', function(req, res) { 88 | // call describe to dynamically generate the form fields 89 | rest.api(req).describe('Account', function(data) { 90 | res.render('new', { title: 'New Account', data: data }) 91 | }); 92 | }); 93 | 94 | // create the account in salesforce 95 | app.post('/accounts/create', function(req, res) { 96 | rest.api(req).create("Account", req.body.account, function(results) { 97 | if (results.success == true) { 98 | res.redirect('/accounts/'+results.id); 99 | res.end(); 100 | } 101 | }); 102 | }); 103 | 104 | // display the account 105 | app.get('/accounts/:id', function(req, res) { 106 | rest.api(req).retrieve('Account', req.params.id, null, function(data) { 107 | res.render('show', { title: 'Account Details', data: data }); 108 | }); 109 | }); 110 | 111 | // form to update an existing account 112 | app.get('/accounts/:id/edit', function(req, res) { 113 | rest.api(req).retrieve('Account', req.params.id, null, function(data) { 114 | res.render('edit', { title: 'Edit Account', data: data }); 115 | }); 116 | }); 117 | 118 | // update teh account in salesforce 119 | app.post('/accounts/:id/update', function(req, res) { 120 | rest.api(req).update("Account", req.params.id, req.body.account, function(results) { 121 | res.redirect('/accounts/'+req.params.id); 122 | res.end(); 123 | }); 124 | }); 125 | 126 | app.listen(port, function(){ 127 | console.log("Express server listening on port %d in %s mode", app.address().port, app.settings.env); 128 | }); 129 | -------------------------------------------------------------------------------- /rest.js: -------------------------------------------------------------------------------- 1 | var rest = require('restler'); 2 | var oauth = require('./oauth.js'); 3 | 4 | exports.request = function request(options) { 5 | // TODO - API version 6 | // Handle a fully qualified path (for id url) versus relative path 7 | var restUrl = (options.path.substr(0, 6) == "https:" ? options.path : options.oauth.instance_url + '/services/data' + options.path); 8 | console.log("\n\nrest.request - method: " + options.method + " restUrl: " + restUrl + ", data: " + options.data); 9 | 10 | rest.request(restUrl, { 11 | method: options.method, 12 | data: options.data, 13 | headers: { 14 | 'Accept':'application/json', 15 | 'Authorization':'OAuth ' + options.oauth.access_token, 16 | 'Content-Type': 'application/json', 17 | } 18 | }).on('complete', function(data, response) { 19 | if (response.statusCode >= 200 && response.statusCode < 300) { 20 | console.log("REST Response: " + data); 21 | if (data.length == 0) { 22 | options.callback(); 23 | } else { 24 | options.callback(JSON.parse(data)); 25 | } 26 | } 27 | }).on('error', function(data, response) { 28 | console.error(data); 29 | if (response.statusCode == 401) { 30 | // Session expired or invalid 31 | if ( options.retry || ! options.refresh ) { 32 | console.log("Invalid session - we tried!"); 33 | // We already tried, or there is no refresh callback 34 | options.error(data, response); 35 | } else { 36 | // We use a refresh callback from options to decouple 37 | // rest from oauth 38 | console.log("Invalid session - trying a refresh"); 39 | options.refresh(function(oauth){ 40 | options.oauth.access_token = oauth.access_token; 41 | options.retry = true; 42 | request(options); 43 | }); 44 | } 45 | } else { 46 | options.error(data, response); 47 | } 48 | }); 49 | }; 50 | 51 | // token can be an oauth object (with access_token etc properties), an object 52 | // with an oauth property (like a request), or a string containing an access token 53 | exports.api = function api(token, refresh, apiVersion) { 54 | var oauth = (token.access_token) ? 55 | token : 56 | (token.oauth || { access_token: token }); 57 | 58 | refresh = refresh || function(callback) { 59 | oauth.refresh({ 60 | oauth: oauthObj, 61 | callback: callback 62 | }); 63 | } 64 | 65 | 66 | apiVersion = apiVersion || '22.0'; 67 | 68 | return { 69 | versions: function versions(callback, error) { 70 | var options = { 71 | oauth: oauth, 72 | refresh: refresh, 73 | path: '/', 74 | callback: callback, 75 | error: error 76 | } 77 | exports.request(options); 78 | }, 79 | 80 | resources: function resources(callback, error) { 81 | var options = { 82 | oauth: oauth, 83 | refresh: refresh, 84 | path: '/v' + apiVersion + '/', 85 | callback: callback, 86 | error: error 87 | } 88 | exports.request(options); 89 | }, 90 | 91 | describeGlobal: function describeGlobal(callback, error) { 92 | var options = { 93 | oauth: oauth, 94 | refresh: refresh, 95 | path: '/v' + apiVersion + '/sobjects/', 96 | callback: callback, 97 | error: error 98 | } 99 | exports.request(options); 100 | }, 101 | 102 | identity: function identity(callback, error) { 103 | var options = { 104 | oauth:oauth, 105 | refresh:refresh, 106 | path:oauth.id, 107 | callback:callback, 108 | error:error 109 | } 110 | exports.request(options); 111 | }, 112 | 113 | metadata: function metadata(objtype, callback, error) { 114 | var options = { 115 | oauth: oauth, 116 | refresh: refresh, 117 | path: '/v' + apiVersion + '/sobjects/' + objtype + '/', 118 | callback: callback, 119 | error: error 120 | } 121 | exports.request(options); 122 | }, 123 | 124 | describe: function describe(objtype, callback, error) { 125 | var options = { 126 | oauth: oauth, 127 | refresh: refresh, 128 | path: '/v' + apiVersion + '/sobjects/' + objtype + '/describe/', 129 | callback: callback, 130 | error: error 131 | } 132 | exports.request(options); 133 | }, 134 | 135 | create: function create(objtype, fields, callback, error) { 136 | var options = { 137 | oauth: oauth, 138 | refresh: refresh, 139 | path: '/v' + apiVersion + '/sobjects/' + objtype + '/', 140 | callback: callback, 141 | error: error, 142 | method: 'POST', 143 | data: JSON.stringify(fields) 144 | } 145 | exports.request(options); 146 | }, 147 | 148 | retrieve: function retrieve(objtype, id, fields, callback, error) { 149 | if (typeof fields === 'function') { 150 | // fields param missing 151 | error = callback; 152 | callback = fields; 153 | fields = null; 154 | } 155 | 156 | var options = { 157 | oauth: oauth, 158 | refresh: refresh, 159 | path: '/v' + apiVersion + '/sobjects/' + objtype + '/' + id 160 | + (fields ? '?fields=' + fields : ''), 161 | callback: callback, 162 | error: error 163 | } 164 | exports.request(options); 165 | }, 166 | 167 | upsert: function upsert(objtype, externalIdField, externalId, fields, callback, error) { 168 | var options = { 169 | oauth: oauth, 170 | refresh: refresh, 171 | path: '/v' + apiVersion + '/sobjects/' + objtype + '/' + externalIdField + '/' + externalId, 172 | callback: callback, 173 | error: error, 174 | method: 'PATCH', 175 | data: JSON.stringify(fields) 176 | } 177 | exports.request(options); 178 | }, 179 | 180 | update: function update(objtype, id, fields, callback, error) { 181 | var options = { 182 | oauth: oauth, 183 | refresh: refresh, 184 | path: '/v' + apiVersion + '/sobjects/' + objtype + '/' + id, 185 | callback: callback, 186 | error: error, 187 | method: 'PATCH', 188 | data: JSON.stringify(fields) 189 | } 190 | exports.request(options); 191 | }, 192 | 193 | del: function del(objtype, id, callback, error) { 194 | var options = { 195 | oauth: oauth, 196 | refresh: refresh, 197 | path: '/v' + apiVersion + '/sobjects/' + objtype + '/' + id, 198 | callback: callback, 199 | error: error, 200 | method: 'DELETE' 201 | } 202 | exports.request(options); 203 | }, 204 | 205 | search: function search(sosl, callback, error) { 206 | var options = { 207 | oauth: oauth, 208 | refresh: refresh, 209 | path: '/v' + apiVersion + '/search/?q=' + escape(sosl), 210 | callback: callback, 211 | error: error 212 | } 213 | exports.request(options); 214 | }, 215 | 216 | query: function query(soql, callback, error) { 217 | var options = { 218 | oauth: oauth, 219 | refresh: refresh, 220 | path: '/v' + apiVersion + '/query/?q=' + escape(soql), 221 | callback: callback, 222 | error: error 223 | } 224 | exports.request(options); 225 | }, 226 | 227 | recordFeed: function recordFeed(id, callback, error) { 228 | var options = { 229 | oauth: oauth, 230 | refresh: refresh, 231 | path: '/v' + apiVersion + '/chatter/feeds/record/' + id + '/feed-items', 232 | callback: callback, 233 | error: error 234 | } 235 | exports.request(options); 236 | }, 237 | 238 | newsFeed: function newsFeed(id, callback, error) { 239 | var options = { 240 | oauth: oauth, 241 | refresh:refresh, 242 | path: '/v' + apiVersion + '/chatter/feeds/news/' + id + '/feed-items', 243 | callback:callback, 244 | error:error 245 | } 246 | exports.request(options); 247 | }, 248 | 249 | profileFeed: function profileFeed(id, callback, error) { 250 | var options = { 251 | oauth: oauth, 252 | refresh: refresh, 253 | path: '/v' + apiVersion + '/chatter/feeds/user-profile/' + id + '/feed-items', 254 | callback: callback, 255 | error: error 256 | } 257 | exports.request(options); 258 | } 259 | }; 260 | }; 261 | 262 | --------------------------------------------------------------------------------