├── .gitignore
├── css
└── style.css
├── img
└── ionic.png
├── js
├── config.js
├── controllers.js
├── app.js
└── forceng.js
├── bootconfig.json
├── templates
├── account-list.html
├── account.html
├── contact-list.html
├── menu.html
├── contact.html
└── edit-contact.html
├── index.html
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .gitignore
--------------------------------------------------------------------------------
/css/style.css:
--------------------------------------------------------------------------------
1 | /* Empty. Add your own CSS if you like */
2 |
--------------------------------------------------------------------------------
/img/ionic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ionic-team/ionic-starter-salesforce/HEAD/img/ionic.png
--------------------------------------------------------------------------------
/js/config.js:
--------------------------------------------------------------------------------
1 | angular.module('config', [])
2 |
3 | .constant('forcengOptions', {})
4 |
5 | // baseURL should be left to empty string. This value is only used when you want to use the same app in a Visualforce
6 | // page where you have to account for the path to the static resource. In that case the config module is created from
7 | // within index.vf where the path to the static resource can be obtained.
8 | .constant('baseURL', '');
9 |
10 |
--------------------------------------------------------------------------------
/bootconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "remoteAccessConsumerKey": "3MVG9Iu66FKeHhINkB1l7xt7kR8czFcCTUhgoA8Ol2Ltf1eYHOU4SqQRSEitYFDUpqRWcoQ2.dBv_a1Dyu5xa",
3 | "oauthRedirectURI": "testsfdc:///mobilesdk/detect/oauth/done",
4 | "oauthScopes": [
5 | "web",
6 | "api"
7 | ],
8 | "isLocal": true,
9 | "startPage": "index.html",
10 | "errorPage": "error.html",
11 | "shouldAuthenticate": true,
12 | "attemptOfflineLoad": false
13 | }
--------------------------------------------------------------------------------
/templates/account-list.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
12 |
13 |
--------------------------------------------------------------------------------
/templates/account.html:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
--------------------------------------------------------------------------------
/templates/contact-list.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/templates/menu.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | Menu
17 |
18 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/templates/contact.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Edit
5 |
6 |
7 |
27 |
28 |
--------------------------------------------------------------------------------
/templates/edit-contact.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/js/controllers.js:
--------------------------------------------------------------------------------
1 | angular.module('starter.controllers', ['forceng'])
2 |
3 | .controller('AppCtrl', function ($scope, force) {
4 |
5 | $scope.logout = function() {
6 | force.logout();
7 | };
8 |
9 | })
10 |
11 | .controller('ContactListCtrl', function ($scope, force) {
12 |
13 | force.query('select id, name, title from contact limit 50').then(
14 | function (data) {
15 | $scope.contacts = data.records;
16 | },
17 | function (error) {
18 | alert("Error Retrieving Contacts");
19 | console.log(error);
20 | });
21 |
22 | })
23 |
24 | .controller('ContactCtrl', function ($scope, $stateParams, force) {
25 |
26 | force.retrieve('contact', $stateParams.contactId, 'id,name,title,phone,mobilephone,email').then(
27 | function (contact) {
28 | $scope.contact = contact;
29 | });
30 |
31 |
32 | })
33 |
34 | .controller('EditContactCtrl', function ($scope, $stateParams, $ionicNavBarDelegate, force) {
35 |
36 | force.retrieve('contact', $stateParams.contactId, 'id,firstname,lastname,title,phone,mobilephone,email').then(
37 | function (contact) {
38 | $scope.contact = contact;
39 | });
40 |
41 | $scope.save = function () {
42 | force.update('contact', $scope.contact).then(
43 | function (response) {
44 | $ionicNavBarDelegate.back();
45 | },
46 | function() {
47 | alert("An error has occurred.");
48 | });
49 | };
50 |
51 | })
52 |
53 | .controller('CreateContactCtrl', function ($scope, $stateParams, $ionicNavBarDelegate, force) {
54 |
55 | $scope.contact = {};
56 |
57 | $scope.save = function () {
58 | force.create('contact', $scope.contact).then(
59 | function (response) {
60 | $ionicNavBarDelegate.back();
61 | },
62 | function() {
63 | alert("An error has occurred.");
64 | });
65 | };
66 |
67 | })
68 |
69 | .controller('AccountListCtrl', function ($scope, force) {
70 |
71 | force.query('select id, name from account limit 50').then(
72 | function (data) {
73 | $scope.accounts = data.records;
74 | });
75 |
76 | })
77 |
78 | .controller('AccountCtrl', function ($scope, $stateParams, force) {
79 |
80 | force.retrieve('account', $stateParams.accountId, 'id,name,phone,billingaddress').then(
81 | function (account) {
82 | $scope.account = account;
83 | });
84 |
85 | });
86 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Starter pack for building Ionic apps on top of the Salesforce platform created as a partnership between Ionic and Salesforce.
2 |
3 | This is an addon starter template for the [Ionic Framework](http://ionicframework.com/). The template installs a starter application that uses Salesforce OAuth to authenticate, and the Salesforce REST APIs to access and manipulate Salesforce data.
4 |
5 | ## How to use this template
6 |
7 | To use this starter pack, you'll need to have [installed Ionic](http://ionicframework.com/getting-started) and Cordova:
8 |
9 | ```bash
10 | $ sudo npm install -g ionic cordova
11 | ```
12 |
13 | Then, all you need to do is run:
14 | ```
15 | $ ionic start myApp salesforce
16 | $ cd myApp
17 | $ ionic platforms update ios
18 | $ ionic plugin add https://github.com/forcedotcom/SalesforceMobileSDK-CordovaPlugin
19 | ```
20 |
21 | ## Running on device
22 |
23 | Substitute ios for android in the instructioons below, but if you can, the ios development toolchain is a lot easier to work with until you need to do anything custom to Android.
24 |
25 | 1. Build the project:
26 |
27 | ```bash
28 | $ ionic build ios
29 | ```
30 |
31 | 1. Run the app in the emulator
32 |
33 | ```bash
34 | $ ionic emulate ios
35 | ```
36 |
37 | 1. ... or (for iOS) open **platforms/ios/myApp.xcodeproj** in Xcode and run the app on your device. If the build fails in Xcode, select the myApp target, click the **Build Settings** tab, search for **bitcode**, select **No** for **Enable Bitcode**, and try again.
38 |
39 |
40 | ## Running in the browser
41 |
42 | Because of the browser's cross-origin restrictions, your Ionic application hosted on your own server (or localhost) will not be able to make API calls directly to the *.salesforce.com domain. The solution is to proxy your API calls through your own server. You can use your own proxy server or use [ForceServer](https://github.com/ccoenraets/force-server), a simple development server for Force.com. It provides two main features:
43 |
44 | - A **Proxy Server** to avoid cross-domain policy issues when invoking Salesforce REST services. (The Chatter API supports CORS, but other APIs don’t yet)
45 | - A **Local Web Server** to (1) serve the OAuth callback URL defined in your Connected App, and (2) serve the whole app during development and avoid cross-domain policy issues when loading files (for example, templates) from the local file system.
46 |
47 | To run the application in the browser using ForceServer:
48 |
49 | 1. Install ForceServer
50 |
51 | ```bash
52 | $ sudo npm install -g force-server
53 | ```
54 |
55 | 2. Navigate (cd) to your Ionic app's **www** directory
56 |
57 | 3. Start the server
58 |
59 | ```
60 | force-server
61 | ```
62 |
63 | This command will start the server on port 8200, and automatically load your app (http://localhost:8200) in a browser window. You'll see the Salesforce login window (make sure you enable the popup), and the list of contacts will appear after you log in. If you don’t have a free Salesforce Developer Edition to log in to, you can create one [here](http://developer.salesforce.com/signup).
64 |
65 | You can change the port number and the web root. Type the following command for more info:
66 |
67 | ```
68 | force-server --help
69 | ```
70 |
71 | ## OAuth and REST
72 |
73 | This template uses [forceng](https://github.com/ccoenraets/forceng), a micro-library that makes it easy to use the Salesforce REST APIs in AngularJS applications. forceng allows you to easily login into Salesforce using OAuth, and to access your Salesforce data using a simple API.
74 |
--------------------------------------------------------------------------------
/js/app.js:
--------------------------------------------------------------------------------
1 | // Ionic Starter App
2 |
3 | // angular.module is a global place for creating, registering and retrieving Angular modules
4 | // 'starter' is the name of this angular module example (also set in a attribute in index.html)
5 | // the 2nd parameter is an array of 'requires'
6 | // 'starter.controllers' is found in controllers.js
7 | angular.module('starter', ['ionic', 'forceng', 'starter.controllers', 'config'])
8 |
9 | .run(function ($ionicPlatform, $state, force, forcengOptions) {
10 |
11 | $ionicPlatform.ready(function () {
12 |
13 | // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard
14 | // for form inputs)
15 | if (window.cordova && window.cordova.plugins && window.cordova.plugins.Keyboard) {
16 | window.cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
17 | window.cordova.plugins.Keyboard.disableScroll(true);
18 | }
19 | if (window.StatusBar) {
20 | // org.apache.cordova.statusbar required
21 | StatusBar.styleDefault();
22 | }
23 |
24 | // Initialize forceng
25 | force.init(forcengOptions);
26 |
27 | if (forcengOptions.accessToken) {
28 | // If the accessToken was provided (typically when running the app from within a Visualforce page,
29 | // go straight to the contact list
30 | $state.go('app.contactlist');
31 | } else {
32 | // Otherwise (the app is probably running as a standalone web app or as a hybrid local app with the
33 | // Mobile SDK, login first.)
34 | force.login().then(
35 | function () {
36 | $state.go('app.contactlist');
37 | },
38 | function(error) {
39 | alert("Login was not successful");
40 | });
41 | }
42 |
43 | });
44 | })
45 |
46 | .config(function ($stateProvider, $urlRouterProvider, baseURL) {
47 |
48 | // baseURL (defined in the config.js module) is only there to support running the same app as a Mobile SDK
49 | // hybrid local and hybrid remote app (where the app is run from withing a Visualforce page). When running the
50 | // app inside a Visualforce page, you have to account for the path of the app's static resource. To accomplish
51 | // that, you create the config module from within the VF page (as opposed to importing config.js), and set
52 | // baseURL to the app's static resource path.
53 |
54 | $stateProvider
55 |
56 | .state('app', {
57 | url: "/app",
58 | abstract: true,
59 | templateUrl: baseURL + "templates/menu.html",
60 | controller: 'AppCtrl'
61 | })
62 |
63 | .state('app.contactlist', {
64 | url: "/contactlist",
65 | views: {
66 | 'menuContent': {
67 | templateUrl: baseURL + "templates/contact-list.html",
68 | controller: 'ContactListCtrl'
69 | }
70 | }
71 | })
72 |
73 | .state('app.contact', {
74 | url: "/contacts/:contactId",
75 | views: {
76 | 'menuContent': {
77 | templateUrl: baseURL + "templates/contact.html",
78 | controller: 'ContactCtrl'
79 | }
80 | }
81 | })
82 |
83 | .state('app.edit-contact', {
84 | url: "/edit-contact/:contactId",
85 | views: {
86 | 'menuContent': {
87 | templateUrl: baseURL + "templates/edit-contact.html",
88 | controller: 'EditContactCtrl'
89 | }
90 | }
91 | })
92 |
93 | .state('app.add-contact', {
94 | url: "/create-contact",
95 | views: {
96 | 'menuContent': {
97 | templateUrl: baseURL + "templates/edit-contact.html",
98 | controller: 'CreateContactCtrl'
99 | }
100 | }
101 | })
102 |
103 | .state('app.accountlist', {
104 | url: "/accountlist",
105 | views: {
106 | 'menuContent': {
107 | templateUrl: baseURL + "templates/account-list.html",
108 | controller: 'AccountListCtrl'
109 | }
110 | }
111 | })
112 |
113 | .state('app.account', {
114 | url: "/accounts/:accountId",
115 | views: {
116 | 'menuContent': {
117 | templateUrl: baseURL + "templates/account.html",
118 | controller: 'AccountCtrl'
119 | }
120 | }
121 | });
122 |
123 | });
124 |
--------------------------------------------------------------------------------
/js/forceng.js:
--------------------------------------------------------------------------------
1 | /**
2 | * ForceNG - REST toolkit for Salesforce.com
3 | * Author: Christophe Coenraets @ccoenraets
4 | * Version: 0.6.1
5 | */
6 | angular.module('forceng', [])
7 |
8 | .factory('force', function ($rootScope, $q, $window, $http) {
9 |
10 | // The login URL for the OAuth process
11 | // To override default, pass loginURL in init(props)
12 | var loginURL = 'https://login.salesforce.com',
13 |
14 | // The Connected App client Id. Default app id provided - Not for production use.
15 | // This application supports http://localhost:8200/oauthcallback.html as a valid callback URL
16 | // To override default, pass appId in init(props)
17 | appId = '3MVG9fMtCkV6eLheIEZplMqWfnGlf3Y.BcWdOf1qytXo9zxgbsrUbS.ExHTgUPJeb3jZeT8NYhc.hMyznKU92',
18 |
19 | // The force.com API version to use.
20 | // To override default, pass apiVersion in init(props)
21 | apiVersion = 'v33.0',
22 |
23 | // Keep track of OAuth data (access_token, refresh_token, and instance_url)
24 | oauth,
25 |
26 | // By default we store fbtoken in sessionStorage. This can be overridden in init()
27 | tokenStore = {},
28 |
29 | // if page URL is http://localhost:3000/myapp/index.html, context is /myapp
30 | context = window.location.pathname.substring(0, window.location.pathname.lastIndexOf("/")),
31 |
32 | // if page URL is http://localhost:3000/myapp/index.html, serverURL is http://localhost:3000
33 | serverURL = window.location.protocol + '//' + window.location.hostname + (window.location.port ? ':' + window.location.port : ''),
34 |
35 | // if page URL is http://localhost:3000/myapp/index.html, baseURL is http://localhost:3000/myapp
36 | baseURL = serverURL + context,
37 |
38 | // Only required when using REST APIs in an app hosted on your own server to avoid cross domain policy issues
39 | // To override default, pass proxyURL in init(props)
40 | proxyURL = baseURL,
41 |
42 | // if page URL is http://localhost:3000/myapp/index.html, oauthCallbackURL is http://localhost:3000/myapp/oauthcallback.html
43 | // To override default, pass oauthCallbackURL in init(props)
44 | oauthCallbackURL = baseURL + '/oauthcallback.html',
45 |
46 | // Because the OAuth login spans multiple processes, we need to keep the login success and error handlers as a variables
47 | // inside the module instead of keeping them local within the login function.
48 | deferredLogin,
49 |
50 | // Reference to the Salesforce OAuth plugin
51 | oauthPlugin,
52 |
53 | // Whether or not to use a CORS proxy. Defaults to false if app running in Cordova or in a VF page
54 | // Can be overriden in init()
55 | useProxy = (window.cordova || window.SfdcApp) ? false : true;
56 |
57 | /*
58 | * Determines the request base URL.
59 | */
60 | function getRequestBaseURL() {
61 |
62 | var url;
63 |
64 | if (useProxy) {
65 | url = proxyURL;
66 | } else if (oauth.instance_url) {
67 | url = oauth.instance_url;
68 | } else {
69 | url = serverURL;
70 | }
71 |
72 | // dev friendly API: Remove trailing '/' if any so url + path concat always works
73 | if (url.slice(-1) === '/') {
74 | url = url.slice(0, -1);
75 | }
76 |
77 | return url;
78 | }
79 |
80 | function parseQueryString(queryString) {
81 | var qs = decodeURIComponent(queryString),
82 | obj = {},
83 | params = qs.split('&');
84 | params.forEach(function (param) {
85 | var splitter = param.split('=');
86 | obj[splitter[0]] = splitter[1];
87 | });
88 | return obj;
89 | }
90 |
91 | function toQueryString(obj) {
92 | var parts = [],
93 | i;
94 | for (i in obj) {
95 | if (obj.hasOwnProperty(i)) {
96 | parts.push(encodeURIComponent(i) + "=" + encodeURIComponent(obj[i]));
97 | }
98 | }
99 | return parts.join("&");
100 | }
101 |
102 | function refreshTokenWithPlugin(deferred) {
103 | oauthPlugin.authenticate(
104 | function (response) {
105 | oauth.access_token = response.accessToken;
106 | tokenStore.forceOAuth = JSON.stringify(oauth);
107 | deferred.resolve();
108 | },
109 | function () {
110 | console.log('Error refreshing oauth access token using the oauth plugin');
111 | deferred.reject();
112 | });
113 | }
114 |
115 | function refreshTokenWithHTTPRequest(deferred) {
116 | var params = {
117 | 'grant_type': 'refresh_token',
118 | 'refresh_token': oauth.refresh_token,
119 | 'client_id': appId
120 | },
121 |
122 | headers = {},
123 |
124 | url = useProxy ? proxyURL : loginURL;
125 |
126 | // dev friendly API: Remove trailing '/' if any so url + path concat always works
127 | if (url.slice(-1) === '/') {
128 | url = url.slice(0, -1);
129 | }
130 |
131 | url = url + '/services/oauth2/token?' + toQueryString(params);
132 |
133 | if (!useProxy) {
134 | headers["Target-URL"] = loginURL;
135 | }
136 |
137 | $http({
138 | headers: headers,
139 | method: 'POST',
140 | url: url,
141 | params: params
142 | })
143 | .success(function (data, status, headers, config) {
144 | console.log('Token refreshed');
145 | oauth.access_token = data.access_token;
146 | tokenStore.forceOAuth = JSON.stringify(oauth);
147 | deferred.resolve();
148 | })
149 | .error(function (data, status, headers, config) {
150 | console.log('Error while trying to refresh token');
151 | deferred.reject();
152 | });
153 | }
154 |
155 | function refreshToken() {
156 | var deferred = $q.defer();
157 | if (oauthPlugin) {
158 | refreshTokenWithPlugin(deferred);
159 | } else {
160 | refreshTokenWithHTTPRequest(deferred);
161 | }
162 | return deferred.promise;
163 | }
164 |
165 | /**
166 | * Initialize ForceNG
167 | * @param params
168 | * appId (optional)
169 | * loginURL (optional)
170 | * proxyURL (optional)
171 | * oauthCallbackURL (optional)
172 | * apiVersion (optional)
173 | * accessToken (optional)
174 | * instanceURL (optional)
175 | * refreshToken (optional)
176 | */
177 | function init(params) {
178 |
179 | if (params) {
180 | appId = params.appId || appId;
181 | apiVersion = params.apiVersion || apiVersion;
182 | loginURL = params.loginURL || loginURL;
183 | oauthCallbackURL = params.oauthCallbackURL || oauthCallbackURL;
184 | proxyURL = params.proxyURL || proxyURL;
185 | useProxy = params.useProxy === undefined ? useProxy : params.useProxy;
186 |
187 | if (params.accessToken) {
188 | if (!oauth) oauth = {};
189 | oauth.access_token = params.accessToken;
190 | }
191 |
192 | if (params.instanceURL) {
193 | if (!oauth) oauth = {};
194 | oauth.instance_url = params.instanceURL;
195 | }
196 |
197 | if (params.refreshToken) {
198 | if (!oauth) oauth = {};
199 | oauth.refresh_token = params.refreshToken;
200 | }
201 | }
202 |
203 | console.log("useProxy: " + useProxy);
204 | }
205 |
206 | /**
207 | * Discard the OAuth access_token. Use this function to test the refresh token workflow.
208 | */
209 | function discardToken() {
210 | delete oauth.access_token;
211 | tokenStore.forceOAuth = JSON.stringify(oauth);
212 | }
213 |
214 | /**
215 | * Called internally either by oauthcallback.html (when the app is running the browser)
216 | * @param url - The oauthCallbackURL called by Salesforce at the end of the OAuth workflow. Includes the access_token in the querystring
217 | */
218 | function oauthCallback(url) {
219 |
220 | // Parse the OAuth data received from Facebook
221 | var queryString,
222 | obj;
223 |
224 | if (url.indexOf("access_token=") > 0) {
225 | queryString = url.substr(url.indexOf('#') + 1);
226 | obj = parseQueryString(queryString);
227 | oauth = obj;
228 | tokenStore['forceOAuth'] = JSON.stringify(oauth);
229 | if (deferredLogin) deferredLogin.resolve();
230 | } else if (url.indexOf("error=") > 0) {
231 | queryString = decodeURIComponent(url.substring(url.indexOf('?') + 1));
232 | obj = parseQueryString(queryString);
233 | if (deferredLogin) deferredLogin.reject(obj);
234 | } else {
235 | if (deferredLogin) deferredLogin.reject({status: 'access_denied'});
236 | }
237 | }
238 |
239 | /**
240 | * Login to Salesforce using OAuth. If running in a Browser, the OAuth workflow happens in a a popup window.
241 | */
242 | function login() {
243 | deferredLogin = $q.defer();
244 | if (window.cordova) {
245 | loginWithPlugin();
246 | } else {
247 | loginWithBrowser();
248 | }
249 | return deferredLogin.promise;
250 | }
251 |
252 | function loginWithPlugin() {
253 | document.addEventListener("deviceready", function () {
254 | oauthPlugin = cordova.require("com.salesforce.plugin.oauth");
255 | if (!oauthPlugin) {
256 | console.error('Salesforce Mobile SDK OAuth plugin not available');
257 | if (deferredLogin) deferredLogin.reject({status: 'Salesforce Mobile SDK OAuth plugin not available'});
258 | return;
259 | }
260 | oauthPlugin.getAuthCredentials(
261 | function (creds) {
262 | // Initialize ForceJS
263 | init({accessToken: creds.accessToken, instanceURL: creds.instanceUrl, refreshToken: creds.refreshToken});
264 | if (deferredLogin) deferredLogin.resolve();
265 | },
266 | function (error) {
267 | console.log(error);
268 | if (deferredLogin) deferredLogin.reject(error);
269 | }
270 | );
271 | }, false);
272 | }
273 |
274 | function loginWithBrowser() {
275 | console.log('loginURL: ' + loginURL);
276 | console.log('oauthCallbackURL: ' + oauthCallbackURL);
277 |
278 | var loginWindowURL = loginURL + '/services/oauth2/authorize?client_id=' + appId + '&redirect_uri=' +
279 | oauthCallbackURL + '&response_type=token';
280 | window.open(loginWindowURL, '_blank', 'location=no');
281 | }
282 |
283 | /**
284 | * Gets the user's ID (if logged in)
285 | * @returns {string} | undefined
286 | */
287 | function getUserId() {
288 | return (typeof(oauth) !== 'undefined') ? oauth.id.split('/').pop() : undefined;
289 | }
290 |
291 | /**
292 | * Check the login status
293 | * @returns {boolean}
294 | */
295 | function isAuthenticated() {
296 | return (oauth && oauth.access_token) ? true : false;
297 | }
298 |
299 | /**
300 | * Lets you make any Salesforce REST API request.
301 | * @param obj - Request configuration object. Can include:
302 | * method: HTTP method: GET, POST, etc. Optional - Default is 'GET'
303 | * path: path in to the Salesforce endpoint - Required
304 | * params: queryString parameters as a map - Optional
305 | * data: JSON object to send in the request body - Optional
306 | */
307 | function request(obj) {
308 |
309 | var method = obj.method || 'GET',
310 | headers = {},
311 | url = getRequestBaseURL(),
312 | deferred = $q.defer();
313 |
314 | if (!oauth || (!oauth.access_token && !oauth.refresh_token)) {
315 | deferred.reject('No access token. Login and try again.');
316 | return deferred.promise;
317 | }
318 |
319 | // dev friendly API: Add leading '/' if missing so url + path concat always works
320 | if (obj.path.charAt(0) !== '/') {
321 | obj.path = '/' + obj.path;
322 | }
323 |
324 | url = url + obj.path;
325 |
326 | headers["Authorization"] = "Bearer " + oauth.access_token;
327 | if (obj.contentType) {
328 | headers["Content-Type"] = obj.contentType;
329 | }
330 | if (useProxy) {
331 | headers["Target-URL"] = oauth.instance_url;
332 | }
333 |
334 | $http({
335 | headers: headers,
336 | method: method,
337 | url: url,
338 | params: obj.params,
339 | data: obj.data
340 | })
341 | .success(function (data, status, headers, config) {
342 | deferred.resolve(data);
343 | })
344 | .error(function (data, status, headers, config) {
345 | if (status === 401 && oauth.refresh_token) {
346 | refreshToken()
347 | .success(function () {
348 | // Try again with the new token
349 | request(obj);
350 | })
351 | .error(function () {
352 | console.error(data);
353 | deferred.reject(data);
354 | });
355 | } else {
356 | console.error(data);
357 | deferred.reject(data);
358 | }
359 |
360 | });
361 |
362 | return deferred.promise;
363 | }
364 |
365 | /**
366 | * Execute SOQL query
367 | * @param soql
368 | * @returns {*}
369 | */
370 | function query(soql) {
371 |
372 | return request({
373 | path: '/services/data/' + apiVersion + '/query',
374 | params: {q: soql}
375 | });
376 |
377 | }
378 |
379 | /**
380 | * Retrieve a record based on its Id
381 | * @param objectName
382 | * @param id
383 | * @param fields
384 | * @returns {*}
385 | */
386 | function retrieve(objectName, id, fields) {
387 |
388 | return request({
389 | path: '/services/data/' + apiVersion + '/sobjects/' + objectName + '/' + id,
390 | params: fields ? {fields: fields} : undefined
391 | });
392 |
393 | }
394 |
395 | /**
396 | * Create a record
397 | * @param objectName
398 | * @param data
399 | * @returns {*}
400 | */
401 | function create(objectName, data) {
402 |
403 | return request({
404 | method: 'POST',
405 | contentType: 'application/json',
406 | path: '/services/data/' + apiVersion + '/sobjects/' + objectName + '/',
407 | data: data
408 | });
409 |
410 | }
411 |
412 | /**
413 | * Update a record
414 | * @param objectName
415 | * @param data
416 | * @returns {*}
417 | */
418 | function update(objectName, data) {
419 |
420 | var id = data.Id,
421 | fields = angular.copy(data);
422 |
423 | delete fields.attributes;
424 | delete fields.Id;
425 |
426 | return request({
427 | method: 'POST',
428 | contentType: 'application/json',
429 | path: '/services/data/' + apiVersion + '/sobjects/' + objectName + '/' + id,
430 | params: {'_HttpMethod': 'PATCH'},
431 | data: fields
432 | });
433 |
434 | }
435 |
436 | /**
437 | * Delete a record
438 | * @param objectName
439 | * @param id
440 | * @returns {*}
441 | */
442 | function del(objectName, id) {
443 |
444 | return request({
445 | method: 'DELETE',
446 | path: '/services/data/' + apiVersion + '/sobjects/' + objectName + '/' + id
447 | });
448 |
449 | }
450 |
451 | /**
452 | * Upsert a record
453 | * @param objectName
454 | * @param externalIdField
455 | * @param externalId
456 | * @param data
457 | * @returns {*}
458 | */
459 | function upsert(objectName, externalIdField, externalId, data) {
460 |
461 | return request({
462 | method: 'PATCH',
463 | contentType: 'application/json',
464 | path: '/services/data/' + apiVersion + '/sobjects/' + objectName + '/' + externalIdField + '/' + externalId,
465 | data: data
466 | });
467 |
468 | }
469 |
470 | /**
471 | * Convenience function to invoke APEX REST endpoints
472 | * @param pathOrParams
473 | * @param successHandler
474 | * @param errorHandler
475 | */
476 | function apexrest(pathOrParams) {
477 |
478 | var params;
479 |
480 | if (pathOrParams.substring) {
481 | params = {path: pathOrParams};
482 | } else {
483 | params = pathOrParams;
484 |
485 | if (params.path.charAt(0) !== "/") {
486 | params.path = "/" + params.path;
487 | }
488 |
489 | if (params.path.substr(0, 18) !== "/services/apexrest") {
490 | params.path = "/services/apexrest" + params.path;
491 | }
492 | }
493 |
494 | return request(params);
495 | }
496 |
497 | /**
498 | * Convenience function to invoke the Chatter API
499 | * @param params
500 | * @param successHandler
501 | * @param errorHandler
502 | */
503 | function chatter(params) {
504 |
505 | var base = "/services/data/" + apiVersion + "/chatter";
506 |
507 | if (!params || !params.path) {
508 | errorHandler("You must specify a path for the request");
509 | return;
510 | }
511 |
512 | if (params.path.charAt(0) !== "/") {
513 | params.path = "/" + params.path;
514 | }
515 |
516 | params.path = base + params.path;
517 |
518 | return request(params);
519 |
520 | }
521 |
522 | // The public API
523 | return {
524 | init: init,
525 | login: login,
526 | getUserId: getUserId,
527 | isAuthenticated: isAuthenticated,
528 | request: request,
529 | query: query,
530 | create: create,
531 | update: update,
532 | del: del,
533 | upsert: upsert,
534 | retrieve: retrieve,
535 | apexrest: apexrest,
536 | chatter: chatter,
537 | discardToken: discardToken,
538 | oauthCallback: oauthCallback
539 | };
540 |
541 | });
542 |
543 | // Global function called back by the OAuth login dialog
544 | function oauthCallback(url) {
545 | var injector = angular.element(document.body).injector();
546 | injector.invoke(function (force) {
547 | force.oauthCallback(url);
548 | });
549 | }
550 |
--------------------------------------------------------------------------------