├── .gitignore ├── Procfile ├── README.md ├── app.json ├── package.json └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | .DS_Store 4 | /.idea/ 5 | /npm-debug.log 6 | /.atom/ 7 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: npm start 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Salesforce ETL MySQL 2 | -------------------- 3 | 4 | This sample application shows a simple way to use a Node.js app along with Workflow on Salesforce to do Extract, Transform, and Load (ETL) from Salesforce to MySQL. 5 | 6 | ### Run on Heroku 7 | 1. Deploy the app: [![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy) 8 | 9 | 10 | ### Run Locally 11 | 12 | 1. Create a local MySQL database named `demo` 13 | 1. Install the Node.js dependencies: 14 | 15 | npm install 16 | 17 | 1. Run the local dev server: 18 | 19 | npm run dev 20 | 21 | 1. Start an [ngrok](https://ngrok.com/) tunnel: 22 | 23 | ngrok http 5000 24 | 25 | 26 | ### Setup a Salesforce Workflow & Outbound Message 27 | 28 | 1. [Create a new Workflow](https://login.salesforce.com/01Q) 29 | 1. Select the `Contact` object 30 | 1. Give the rule a name 31 | 1. Select `created, and every time it's edited` 32 | 1. In the `Rule Critera` select `forumla evaluates to true` and enter `True` in the formula field 33 | 1. In `Immediate Workflow Actions` select `New Outbound Message` 34 | 1. Give the Outbound Message a name, enter the `Endpoint URL` for either your Heroku app (e.g. `https://foo.herokuapp.com/`) or your ngrok endpoint for local testing (e.g. `https://1234.ngrok.io/`) 35 | 1. Select the `Email`, `FirstName`, and `LastName` fields 36 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Salesforce ETL MySQL", 3 | "description": "Sample Salesforce ETL to MySQL", 4 | "repository": "https://github.com/jamesward/salesforce-etl-mysql", 5 | "website": "http://www.jamesward.com", 6 | "keywords": ["node", "salesforce", "etl", "mysql"], 7 | "addons": ["cleardb:ignite"] 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "salesforce-etl-mysql", 3 | "version": "0.0.1", 4 | "scripts": { 5 | "dev": "node node_modules/nodemon/bin/nodemon.js server.js", 6 | "start": "node server.js" 7 | }, 8 | "engines": { 9 | "node": "6.11.1" 10 | }, 11 | "devDependencies": { 12 | "nodemon": "latest", 13 | "npm-check-updates": "latest" 14 | }, 15 | "dependencies": { 16 | "express": "4.14.0", 17 | "express-xml-bodyparser": "0.3.0", 18 | "mysql": "2.11.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | let app = require('express')(); 2 | let xmlparser = require('express-xml-bodyparser')({explicitArray: false}); 3 | let mysql = require('mysql'); 4 | 5 | let tableName = 'contact'; 6 | 7 | function transform(sobject) { 8 | return { 9 | 'id': sobject['sf:id'], 10 | 'name': sobject['sf:firstname'] + ' ' + sobject['sf:lastname'], 11 | 'email': sobject['sf:email'] 12 | }; 13 | } 14 | 15 | let pool = mysql.createPool(process.env.CLEARDB_DATABASE_URL || 'mysql://root@localhost/demo'); 16 | 17 | // create the table if it doesn't exist 18 | pool.query(`SELECT * FROM ${tableName}`, function(err) { 19 | if ((err != null) && (err.code == 'ER_NO_SUCH_TABLE')) { 20 | pool.query('create table contact (id VARCHAR(18) PRIMARY KEY, name VARCHAR(128), email VARCHAR(128))'); 21 | } 22 | }); 23 | 24 | function ack() { 25 | return ` 26 | 27 | 28 | 29 | true 30 | 31 | 32 | `; 33 | } 34 | 35 | function nack(errorMessage) { 36 | return ` 37 | 38 | 39 | 40 | soap:Receiver 41 | ${errorMessage} 42 | 43 | 44 | `; 45 | } 46 | 47 | app.use(xmlparser); 48 | 49 | app.post('/', function(req, res) { 50 | 51 | try { 52 | let sobject = req.body['soapenv:envelope']['soapenv:body']['notifications']['notification']['sobject']; 53 | let data = transform(sobject); 54 | 55 | pool.query(`INSERT INTO ${tableName} SET ? ON DUPLICATE KEY UPDATE ?`, [data, data], function(err) { 56 | if (err != null) { 57 | console.error('Database error', err.message); 58 | res.status(500).send(nack(err.message)); 59 | } 60 | else { 61 | res.status(200).send(ack()); 62 | } 63 | }); 64 | } 65 | catch (err) { 66 | console.error('Uncaught error', err); 67 | res.status(500).send(nack(err.message)); 68 | } 69 | }); 70 | 71 | app.listen(process.env.PORT || 5000); 72 | --------------------------------------------------------------------------------