├── .gitignore ├── Procfile ├── public ├── screenshot1.png └── node.svg ├── app.json ├── package.json ├── geocode.js ├── views └── index.ejs ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node index.js 2 | -------------------------------------------------------------------------------- /public/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/contactmap-demoapp/master/public/screenshot1.png -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Contact Map Demo App", 3 | "description": "See how to access Salesforce data in Node.js using Heroku Connect. Click 'View it' at the bottom once you've deployed to continue setup.", 4 | "logo": "https://raw.githubusercontent.com/heroku/heroku-connect-quickstart/master/connect-logo.png", 5 | "website": "https://github.com/scottpersinger/contactmap-demoapp", 6 | "repository": "https://github.com/scottpersinger/contactmap-demoapp", 7 | "success_url": "/create", 8 | "addons": ["heroku-postgresql", "herokuconnect:demo"] 9 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-js-getting-started", 3 | "version": "0.1.2", 4 | "description": "A sample Node.js app using Express 4", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js" 8 | }, 9 | "dependencies": { 10 | "async": "^0.9.0", 11 | "ejs": "^2.3.1", 12 | "express": "~4.9.x", 13 | "pg": "^4.3.0", 14 | "pg-hstore": "^2.3.1", 15 | "sequelize": "^2.0.3" 16 | }, 17 | "engines": { 18 | "node": "0.10.x" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/heroku/node-js-getting-started" 23 | }, 24 | "keywords": [ 25 | "node", 26 | "heroku", 27 | "express" 28 | ], 29 | "license": "MIT" 30 | } 31 | -------------------------------------------------------------------------------- /geocode.js: -------------------------------------------------------------------------------- 1 | var https = require('https'); 2 | 3 | var GOOGLEMAPS = 'https://maps.googleapis.com/maps/api/geocode/json?key=AIzaSyB0ymRb6jQAsgtoUnMO10-x6CjUe0PZK88&address='; 4 | 5 | function geocode(address, callback) { 6 | Geocode.find({where: {address: address}}).then(function(geocode) { 7 | if (geocode) { 8 | callback(geocode); 9 | } else { 10 | https.get(GOOGLEMAPS + encodeURIComponent(address), function(res) { 11 | if (res.statusCode < 300) { 12 | var body = ''; 13 | res.on('data', function(chunk) { 14 | body += chunk; 15 | }); 16 | res.on('end', function() { 17 | body = JSON.parse(body); 18 | if (body.results && body.results.length > 0) { 19 | var lat = body.results[0].geometry.location.lat; 20 | var lon = body.results[0].geometry.location.lng; 21 | Geocode.create({address: address, lat: lat, lon: lon}).then(function(geocode) { 22 | callback(geocode); 23 | }); 24 | } else { 25 | callback(null); 26 | } 27 | }); 28 | } 29 | }); 30 | } 31 | }); 32 | } 33 | 34 | module.exports = geocode -------------------------------------------------------------------------------- /views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 10 | 41 | 42 | 43 |
44 |
45 | 46 | -------------------------------------------------------------------------------- /public/node.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 12 | 17 | 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Contact Mapper 2 | 3 | This is a very basic demonstation app for [Heroku Connect](https://www.heroku.com/connect). It is based 4 | on the [Heroku Node starter app](https://github.com/heroku/node-js-getting-started). 5 | 6 | The application relies on Heroku Connect to sync Contact records into Postgres. We load contact 7 | records from the db, then use the Google Geolocation API to geocode the address so we can locate 8 | each contact properly on the map. 9 | 10 | For better performance address locations are stored back in our database so we only have to look 11 | them up once. 12 | 13 | ![alt tag](public/screenshot1.png) 14 | 15 | ## Install 16 | 17 | Use the Heroku Button below to clone the app: 18 | 19 | [![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy?template=https://github.com/scottpersinger/contactmap-demoapp) 20 | 21 | ## Provision Heroku Connect 22 | 23 | Click on the Heroku Connect addon on your new app to access the Heroku Connect dashboard. 24 | 25 | Add a mapping for the `Contact` object in Salesforce, and select these fields: 26 | 27 | ``` 28 | Email 29 | MailingCity 30 | MailingState 31 | MailingStreet 32 | Name 33 | ``` 34 | 35 | And save the mapping. That's it! Once the data is synchronized just reload the Contact Map app to see 200 36 | Contact records placed on a Google Map of the United States. 37 | 38 | ## Understanding the app 39 | 40 | This app accesses data in a Postgres database. To access the database we use 41 | the [Node PG](https://www.npmjs.com/package/pg) postgres driver and the 42 | wonderful [Sequelize](http://sequelize.readthedocs.org/) ORM for Node. 43 | 44 | In [index.js](index.js) we define two model classes. One is the `Contact` 45 | class which pulls records from the `salesforce` schema which is synchronized 46 | by Heroku Connect. The second is the `Geocode` class which is simply used to 47 | store geolocation coordinates retrieved from the Google Geolocation API. 48 | 49 | Finally the [geocde.js](geocode.js) module takes care of sending a `Contact` 50 | address to the Google Geocode API to retrieve the latitude/longitude for 51 | displaying cards on the map. 52 | 53 | The list of Contacts with annotated Lat/Lng coordinates is passed to [index.html](index.html). 54 | The html page contains JS to create the map and annotate it with all the contacts. 55 | 56 | 57 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var Sequelize = require('sequelize'); 2 | var async = require('async'); 3 | var geocode = require('./geocode'); 4 | 5 | // ************** DATABASE MODELS *********************** 6 | 7 | var dburl = process.env.DATABASE_URL || 'postgres://localhost/herokuconnect'; 8 | var db = new Sequelize(dburl, { 9 | dialectOptions: { 10 | ssl: dburl.indexOf('localhost') == -1 11 | }, 12 | logging: false 13 | }); 14 | 15 | 16 | var Contact = db.define('Contact', { 17 | id: Sequelize.INTEGER, 18 | sfid: Sequelize.STRING, 19 | email: Sequelize.STRING, 20 | name: Sequelize.STRING, 21 | mailingcity: Sequelize.STRING, 22 | mailingstate: Sequelize.STRING, 23 | mailingstreet: Sequelize.STRING 24 | }, { 25 | timestamps: false, 26 | freezeTableName: true, 27 | schema: 'salesforce', 28 | tableName: 'contact' 29 | } 30 | ); 31 | 32 | Geocode = db.define('geocode', { 33 | id: Sequelize.INTEGER, 34 | address: Sequelize.STRING, 35 | lat: Sequelize.FLOAT, 36 | lon: Sequelize.FLOAT 37 | }); 38 | 39 | // Create geocode cache table if not exists 40 | db.sync(); 41 | 42 | // ************** GEOCODING LOGIC *********************** 43 | 44 | var contact_locations = []; 45 | 46 | function geocode_contact(contact, callback) { 47 | if (contact.values.mailingstreet || 48 | contact.values.mailingcity || 49 | contact.values.mailingstate) { 50 | 51 | var addr = contact.values.mailingstreet + "," + 52 | contact.values.mailingcity + "," + 53 | contact.values.mailingstate; 54 | 55 | geocode(addr, function(geocode) { 56 | if (geocode) { 57 | callback(null, {name: escape(contact.values.name), lat: geocode.lat, lon:geocode.lon}); 58 | } else { 59 | callback(); 60 | } 61 | }); 62 | } else { 63 | callback(); 64 | } 65 | } 66 | 67 | function load_contacts(callback) { 68 | Contact.findAll({limit:200}).then(function(rows) { 69 | async.map(rows, geocode_contact, callback); 70 | }); 71 | } 72 | 73 | 74 | 75 | // EXPRESS 76 | 77 | var express = require('express'); 78 | var app = express(); 79 | 80 | app.set('port', (process.env.PORT || 5000)); 81 | app.use(express.static(__dirname + '/public')); 82 | app.set('views', './views'); 83 | app.set('view engine', 'ejs'); 84 | 85 | app.get('/', function(req, res) { 86 | load_contacts(function(error, contact_locations) { 87 | console.log("Locations: ", contact_locations); 88 | res.render('index', {contact_locations: contact_locations.filter(function(val) { return val })}); 89 | }); 90 | }); 91 | 92 | app.get('/create', function(req, res){ 93 | var create_url = 'https://connect.heroku.com/dashboard-next/create-connection'; 94 | // Redirect to Heroku Connect dashboard to finish setup 95 | var hostRe = new RegExp(/^([^.]+)\.herokuapp\.com$/); 96 | 97 | var match = req.headers.host.match(hostRe); 98 | 99 | if (match) { 100 | res.redirect(create_url+'?create='+match[1]); 101 | } else { 102 | res.status(400).send("You need to be running on Heroku!"); 103 | } 104 | }); 105 | 106 | app.listen(app.get('port'), function() { 107 | console.log("Node app is running at localhost:" + app.get('port')); 108 | }); 109 | --------------------------------------------------------------------------------