Using the Oracle Mobile Cloud Service SDK with AngularJS and Ionic

Introduction

Oracle’s Mobile Cloud Service (MCS) can be used with any mobile client development tool. To speed up development for the most popular development tools, MCS comes with a Software Development Kit (SDK) for native Android, iOS and Windows development, as well as a JavaScript SDK for hybrid JavaScript-based development. Oracle’s A-Team has gained experience with the MCS JavaScript SDK in combination with various JavaScript toolkits. In a previous post, we discussed how to use the SDK when building Oracle JET applications. In this post we will share code samples, tips and best practices for building a hybrid mobile app using AngularJS (version 1) and Ionic that connects to MCS. We assume you have a basic knowledge of AngularJS and Ionic. Check out their websites for tutorials if you are new to these technologies.

Main Article

In this article, we will first explain how to download and “install” the SDK, and then we will discuss how to use the SDK to authenticate against MCS, connect to your custom REST API’s defined in MCS and how to leverage the MCS storage and analytics platform services. In subsequent articles we will discuss how MCS eases the implementation of push notifications and how the MCS Offline Sync API can be used to make your mobile app usable without internet connection.

Note that all MCS platform services can be accessed through the Oracle MCS REST API, so strictly speaking you do not need an SDK to connect to MCS. However, using the SDK makes your life much easier as we will demonstrate in this article.

We will use a basic CRUD application consisting of the following screens along the way.

HrApp

We used the Ionic Creator tool to quickly create these screens in a visual way using drag and drop, and then exported the project which gave us a complete Angular/Ionic starter project with the pages, and controller and service scaffolds all set up.

Downloading and Configuring the JavaScript Cordova SDK

The Javascript SDK for MCS comes in two flavours, one for JavaScript applications and one for Cordova applications. The MCS Cordova SDK is a super set of the MCS JS SDK as it provides a few more more capabilities that depend on Cordova, such as methods for registering a device for push notifications.  Since we are creating a hybrid mobile app, we choose the Cordova SDK.

To download an SDK for MCS, login to your MCS instance and click on the Get Started button on the homepage. This takes you to a page where you can select your target platform, and download the SDK with or without a quickstart project.

DownloadSDK

The quickstart project doesn’t really follow the best practices outlined in this post, so we will go for the option to download the SDK alone and add it to an existing project. Alternatively, you can click on the Applications option in the hamburger menu, and click on the Download SDK button. This button takes you to a page that lists all the SDK’s and doesn’t have the option to download a quickstarter project.

After downloading the file, we unzip it and copy over the following files:

  • Copy mcs.js and mcs.min.js into a new mcs subfolder under the lib folder of your project
  • Copy the oracle_mobile_cloud_config.js.js file to the js folder.

The folder structure should now look something like this:

McsDirStruc

The folder structure used in this sample app is suitable for smaller tutorial-style applications. If you are planning to build a large Angular/Ionic app you might want to check out the article AngularJS Best Practices: Directory Structure

We add the mcs.js file to the index.html file, and add it above the existing script tags that include the angular controllers, services, etc:

    <script src="lib/mcs/mcs.js"></script>

    <script src="js/app.js"></script>
    <script src="js/controllers.js"></script>
    <script src="js/routes.js"></script>
    <script src="js/services.js"></script>
    <script src="js/directives.js"></script>

In services.js we define an MCS service that will provide all the functions that our apps needs to interact with MCS:

angular.module('app.services', [])
.factory('mcsService', function($q){

    var mcs_config = {
      "logLevel": mcs.logLevelInfo,
      "mobileBackends": {
        "HR": {
          "default": true,
          "baseUrl": "https://mobileportalsetrial.yourdomain.mobileenv.us2.oraclecloud.com:443",
          "applicationKey": "a4a8af19-38f8-4306-9ac6-adcf7a53deff",
          "authorization": {
            "basicAuth": {
              "backendId": "e045cc30-a347-4f7d-a05f-4d285b6a9abb",
              "anonymousToken": "QVRFQU1ERVZfTUVTREVWMV9NT0JJTEVfQU5PTllNT1VTX0FQUElEOnByczcuYXduOXRlUmhp"
            }
          }
        }
      }
    };

    // initialize MCS mobile backend
    mcs.MobileBackendManager.setConfig(mcs_config);
    var mbe = mcs.MobileBackendManager.getMobileBackend('HR');
    mbe.setAuthenticationType("basicAuth");
})

We pass in $q in the service constructor function because we will be using Angular’s promise implementation later on. The structure of the mcs_config variable can be copied from the oracle_mobile_cloud_service_config.js file. This file includes all the declarative SDK configuration settings. Since we are using basic authentication in our app, we left out the configurations required when using OAuth, Facebook or Single-Sign-On (SSO) authentication. The configuration is defined for a backend called HR. This name doesn’t have to match the name of the backend that is actually in MCS, but can if you wish to match it. A description of all possible configuration settings can be found in the chapter Cordova Applications in the MCS Developer’s Guide.

You can find the values of the mcs_config settings baseUrl, backendId and anonymousToken on the overview of your mobile backend in MCS:

MBE-Settings

The applicationKey is only required for push notifications and can be taken from the Clients tab of your mobile backend page.

On lines 22-24 we initialize the mobile backend object by specifying the configuration file, retrieving the HR mobile backend (although uncommon, you can use multiple backends in your application), and setting the authentication type to basic.

With this skeleton mcsService in place, we are ready to use the various functions of the MCS Cordova SDK!

We could have done the SDK configuration directly in oracle_mobile_cloud_service.js as well, and add a reference to this file in index.html.The downside of that approach is that we “pollute” the global space with another global variable mcs_config.  In addition, we prefer to make the mcsService self-contained including the required configuration settings. If you prefer to have the configuration in a separate file, then it is better to create a json file that holds the config object and read the content of this file into your mcs_config variable within the services.js file.
After you copied the mcs_config variable, you can remove file oracle_mobile_cloud_service.js again from your project.

To know which SDK functions are available to us it is useful to consult the Oracle Mobile Cloud Service JavaScript SDK for Cordova Reference.

Authenticating Against MCS

MCS basic authentication provides two ways to authenticate, a “named” authentication using a username and password and a so-called anonymous authentication which uses the anonymousToken that we specified in the mcs_config variable. Anonymous authentication might be convenient during initial development when you did not set up a user realm with your mobile backend yet, or when you want to use another authentication mechanism for your app that is unrelated to MCS.

We first add functions to our mcsService to support both ways of authentication and to be able to logout:

var authenticate = function(username,password) {
   var deferred = $q.defer();
    mbe.Authorization.authenticate(username, password 
    , function(statusCode,data) {deferred.resolve(statusCode,data)}
    , function(statusCode,data) {deferred.reject(statusCode,data)});         
    return deferred.promise;
 };     

 var authenticateAnonymous = function() {
   var deferred = $q.defer();
    mbe.Authorization.authenticateAnonymous(
      function(statusCode,data) {deferred.resolve(statusCode,data)}
    , function(statusCode,data) {deferred.reject(statusCode,data)});         
    return deferred.promise;
 };     

 var logout = function() {
    var deferred = $q.defer();
    mbe.Authorization.logout(
      function(statusCode,data) {deferred.resolve(statusCode,data)}
    , function(statusCode,data) {deferred.reject(statusCode,data)});         
    return deferred.promise;
 };

And to make the functions callable from the controllers, we return a JSON object with the public functions at the end of the mcsService:

return {
   authenticate:authenticate,
   authenticateAnonymous:authenticateAnonymous,
   logout:logout
}

As you can see, we “promisified” the SDK calls that expect success and failure callback handlers. We strongly recommend you to do the same, for one it makes your coding easier, in particular if you want to chain multiple SDK calls together, but even more important, by using the Angular promise implementation the two-way data binding automatically kicks in. In other words, when the asynchronous SDK callback function returns some data and you need to refresh the user interface with that data you do not need to call $scope.$apply to force the UI to be updated. We will see an example of this behavior later on when we discuss invocation of MCS custom API’s.

With the authentication functions in place, we can now define a login function in our login controller that is invoked by the login button:

angular.module('app.controllers', ['app.services']) 
.controller('loginCtrl', function($scope,$state, mcsService) {

    // We could add logic here to pre-populate username, password with previously used values 
    $scope.loginData = {
        username: '',
        password: '',
    };

    $scope.doLogin = function () {
     mcsService.authenticate($scope.loginData.username, $scope.loginData.password)
       .then(function() {$state.go('departments')})
       .catch(function(err) {alert('Username or password is invalid')});  
    }
})

On line 2, we use Angular’s dependency injection feature to inject the mcsService in our controller. The doLogin function calls the authenticate function of our MCS service which returns a promise. And to complete this section, here is the template code used for the login page.

<ion-view title="Login" id="login" class=" ">
    <ion-content padding="true" class="has-header">
        <form ng-submit="doLogin()" id="login-form1" class="list ">
            <ion-list id="login-list2" class=" ">
                <label class="item item-input " id="login-input1">
                    <span class="input-label">Username</span>
                    <input type="text" ng-model="loginData.username" placeholder="">
                </label>
                <label class="item item-input " id="login-input2">
                    <span class="input-label">Password</span>
                    <input type="password" ng-model="loginData.password" placeholder="">
                </label>
            </ion-list>
            <div class="spacer" style="height: 40px;"></div>
            <button type="submit"  class=" button button-positive  button-block ">Log in</button>
        </form>
    </ion-content>
</ion-view>

As you would expect, this is standard Angular/Ionic syntax, nothing special.

Invoking the HR Custom API

The screen shots included above show a page with a list of departments, and by clicking on a department, we can go to a detail page with all department data and a list of employees within the department. We can create, update and delete a department. We will discuss how to add the employee images in the next section “Using the Storage Service”.

To support these pages, we have defined the following REST endpoints in our custom API in MCS:

Endpoint Method Description
/departments GET List of departments with id, name attributes
/departments POST Add new department
/department/{id} GET All department attributes and list of employees in department
/department/{id} PUT Update department
/department/{id} DELETE Delete department

As you can see, the endpoints and payloads nicely map to the screen design. The more your endpoints and payloads are optimized for your mobile application, the faster you can build the mobile application, and the better the performance of your mobile app.

Oracle A-Team has written an article series on creating a mobile-optimized API using Oracle MCS. This series provides guidelines and best practices for creating such an API from initial design to implementation using the NodeJS engine in Oracle MCS.

To access our custom API endpoints through the SDK, we add the following function to our angular mcsService:

var invokeCustomAPI = function(uri,method,payload) {
  var deferred = $q.defer();
   mbe.CustomCode.invokeCustomCodeJSONRequest(uri , method , payload
   , function(statusCode,data) {deferred.resolve(data)}
   , function(statusCode,data) {deferred.reject(statusCode,data)});         
   return deferred.promise;
};

And we need to make this function public by adding it to the JSON object returned by the service:

return {
   authenticate:authenticate,
   authenticateAnonymous:authenticateAnonymous,
   logout:logout,
   invokeCustomAPI:invokeCustomAPI
}

In the departments controller, we add the code that retrieves the list of departments when the controller gets instantiated:

.controller('departmentsCtrl', function($scope,mcsService) {
    $scope.departments = [];
    mcsService.invokeCustomAPI("hr/departments" , "GET" , null)
    .then (function(data) {
          $scope.departments = data;          
    })
    .catch(function(err) {
        console.log('Error calling endpoint /departments: '+err);
    });      
})

Note that we only need to update the departments array on the controller scope object, there is no need to call $scope.$apply to update the bindings in the page. As mentioned before, Angular automatically takes care of this because we “promisified” the SDK callback functions.

And for completeness, here is the departments list snippet in the departments template:

<ion-list id="departments-list1" class=" ">
  <ion-item ng-repeat="dep in departments" id="departments-list-item4" 
    href="#/editDepartment/{{dep.id}}" class="  ">{{dep.id+' '+dep.name}}
  </ion-item>
</ion-list>

The implementation of the edit department page is really very similar, upon intialization we call the /departments/{id} endpoint, and we define functions for saving and removing a department:

.controller('editDepartmentCtrl', function($scope,$state,$stateParams,mcsService) {
    var currentDepId = $stateParams.departmentId;
    $scope.department = {};
    mcsService.invokeCustomAPI("hr/departments/"+currentDepId , "GET" , null)
    .then (function(data) {
          $scope.department = data;        
    })
    .catch(function(err) {
        console.log('Error calling endpoint /departments/'+currentDepId+': '+err);
    });  
    
    $scope.save = function() {
        mcsService.invokeCustomAPI("hr/departments/"+currentDepId , "PUT" , $scope.department)
        .then (function(data) {
            $state.go('menu.departments');
        })
        .catch(function(err) {
            alert('Error saving department: '+err);
        });          
    }

    $scope.delete = function() {
        alert('DELETE');
        mcsService.invokeCustomAPI("hr/departments/"+currentDepId , "DELETE" , null)
        .then (function(data) {
            $state.go('menu.departments');
        })
        .catch(function(err) {
            alert('Error deleting department: '+err);
        });          
    }
})

Using the MCS Storage Service

MCS includes a file storage service where you can store or cache mobile application objects, such as text, JSON, or images. You can define a storage collection and then add files to such a collection. For our sample application, we store the employee images in an MCS collection named HR. The storage ID of each image includes a reference to the employee ID, so we can easily link each employee with his/her photo in the HR collection:

HRCollection

You can upload files to a storage collection using the MCS UI as shown above. When you use the MCS UI, the ID of the storage object is system-generated. This is inconvenient if you want to associate MCS storage objects with the data of your systems of record that you expose through MCS. Fortunately, if you use the PUT method of the storage REST API, you can add new files and determine the storage ID yourself. For example, you can use a CURL command to upload an image to a storage collection like this:

curl -i -X PUT  -u steven.king:AuThyRJL!  -H "Oracle-Mobile-Backend-ID:bcda8418-8c23-4d92-b656-9299d691e120" -H "Content-Type:image/png"  --data-binary @FayWood.png https://mobileportalsetrial1165yourdomain.mobileenv.us2.oraclecloud.com:443/mobile/platform/storage/collections/HR/objects/EmpImg119

To use the storage service in our app, we first add functions to read storage objects in our mcsService:

var getCollection = function(collectionName) {
  var deferred = $q.defer();
  mbe.Storage.getCollection(collectionName, null
  , function(collection) {deferred.resolve(collection)}
  , function(statusCode,data) {deferred.reject(statusCode,data)});         
  return deferred.promise;
};

var getStorageObjectFromCollection = function(collection,storageId) {
  var deferred = $q.defer();
  collection.getObject(storageId
  , function(storageObject) {deferred.resolve(storageObject)}
  , function(statusCode,data) {deferred.reject(statusCode,data)}
  ,'blob');         
  return deferred.promise;
};

var getStorageObject = function(collectionName,storageId) {
    //  This is the officially documented way, but fires redundant REST call:
    //  return getCollection(collectionName).then( function (collection) {
    //      return getStorageObjectFromCollection(collection,storageId)           
    //  })
    var collection = new mcs._StorageCollection({id:collectionName,userIsolated:false},null,mbe.Storage);
    return getStorageObjectFromCollection(collection,storageId);
 };

return {
 authenticate:authenticate,
 authenticateAnonymous:authenticateAnonymous,
 logout:logout,
 invokeCustomAPI:invokeCustomAPI,
 getStorageObject:getStorageObject
}

The “official” way for accessing a storage object is through its collection. This can be done by calling the getCollection method on the Storage object of the SDK. This returns the collection object which holds the metadata of all the objects inside the collection. On this collection object we can then call methods like getObject, postObject and putObject.  In our app, we want to prevent this additional REST call since we are not interested in the collection as a whole. This is why in line 23 we programmatically instantiate the collection object without making a REST call. As indicated  by the underscore, the _StorageCollection constructor function was intended to be “private” but this use case has been identified as valid and it will be made public in the next version of the SDK.

To be able to show the images with the employees, we extend the code in the editDepartment controller as follows:

    var currentDepId = $stateParams.departmentId;
    $scope.images = {};
    $scope.department = {};
    mcsService.invokeCustomAPI("hr/departments/"+currentDepId , "GET" , null)
    .then (function(data) {
          $scope.department = data;        
          data.employees.forEach(function(emp) {
              getEmployeeImage(emp);
          }) 
    })
    .catch(function(err) {
        console.log('Error calling endpoint /departments/'+currentDepId+': '+err);
    });  

    function getEmployeeImage(emp) {
        var storageId =  "EmpImg"+emp.id;
        mcsService.getStorageObject("HR", storageId)
        .then(function(storageObject){
            var url = URL.createObjectURL(storageObject.getPayload());
            $scope.images[emp.id] = url;
         })
         .catch(function(err) {
             console.log('Error getting storage object with ID '+emp.id+': '+err);
         });          
    }

On line 2 we initialize an images JSON object that holds a list of key-value pairs where the key is the employee ID and the value is the image binary converted to a URL so we can use it an image tag on the page. On line 7-8 we loop over the employees array included in the department object returned by the custom API REST call, and for each employee we call function getEmployeeImage. In this function we call getStorageObject and once the REST all has completed we convert the response payload to an URL and add the employee key-value pair to the images object.

The code snippet for the employees list in the Ionic page template looks like this:

<ion-list id="emp-list1" class=" ">
  <ion-item ng-repeat="emp in department.employees" id="demp-list-item4"  class="  ">
    <img  ng-src="{{images[emp.id]}}" style="width:50px;" /> {{emp.firstName+' '+emp.lastName}} 
  </ion-item>
</ion-list>

The sample app only reads files from the HR collection. If your app has functionality that requires uploading new files to a storage collection, you can add the following methods to the mcsService:

var mergeStorageObjectInCollection = function(collection,fileName, payload, mimetype) {
  var deferred = $q.defer();
  var storageObject = new mcs.StorageObject(collection,{id:fileName,name:fileName});
  storageObject.loadPayload(payload, mimetype);        
  collection.putObject(storageObject
  , function(storageObject) {deferred.resolve(storageObject)}
  , function(statusCode,data) {deferred.reject(statusCode,data)});         
  return deferred.promise;
};

var mergeStorageObject = function(collectionName,filename, payload, mimetype) {
    //  This is the officially documented way, but fires redundant REST call:
    //  return getCollection(collectionName).then( function (collection) {
    //      return getStorageObjectFromCollection(collection,storageId)           
    //   })
    var collection = new mcs._StorageCollection({id:collectionName,userIsolated:false},null,mbe.Storage);
    return mergeStorageObjectInCollection(collection,filename, payload, mimetype);
};

The mergeStorageObjectInCollection function uses the putObject method rather than the postObject method on the collection object which allows you to set the storage object ID yourself instead of having MCS auto-generate the ID for you. If you specify a storage object ID that already exists in the collection, the storage object metadata will be updated in MCS and the content will be replaced.

One typical use case is taking a picture with the device camera and upload this picture to MCS. A-Team has written another article on using the MCS JavaScript SDK with Oracle JET, and that article includes a section on using the Cordova camera plugin to implement this use case.

Using MCS Analytics Service

The MCS Analytics platform service is a very powerful tool to get detailed insight in how your mobile app is used. From your mobile app you can send so-called system events like startSession and endSession to get insight in session duration, device properties, location, etc.  Even better, you can send custom events to get very specific information about how the app is used, for example which pages are accessed for how long, which data is viewed the most, etc.

To support the MCS analytics events, we add the following functions to our mcsService:

var logStartSessionEvent = function() {
    mbe.Analytics.startSession();
}

var logEndSessionEvent = function() {
    mbe.Analytics.endSession();
}

var logCustomEvent = function(eventName, properties) {
    var event = new mcs.AnalyticsEvent(eventName);
    event.properties = properties;
    mbe.Analytics.logEvent(event);
}

var flushAnalyticsEvents = function() {
    mbe.Analytics.flush();
}

return {
   ...
   logStartSessionEvent:logStartSessionEvent,
   logEndSessionEvent: logEndSessionEvent,
   logCustomEvent:logCustomEvent,
   flushAnalyticsEvents:flushAnalyticsEvents
}

When you log an event, the event is not yet sent to the MCS server. You can batch up multiple events and then flush them to the server, which is more efficient because all events are then sent in one REST call. If you log a custom event and you didn’t log a startSession event before, the SDK will automatically create a startSession event first.

In our app, we are going to log a custom event in the editDepartmentCtrl controller whenever the user navigates to the departments detail page to view/edit a department:

    mcsService.invokeCustomAPI("hr/departments/"+currentDepId , "GET" , null)
    .then (function(data) {
          $scope.department = data;      
          mcsService.logCustomEvent('ViewDepartment',{user:$scope.userName,department:$scope.department.name});
          mcsService.flushAnalyticsEvents();
          data.employees.forEach(function(emp) {
              getEmployeeImage(emp);
          }) 
    })
    .catch(function(err) {
        console.log('Error calling endpoint /departments/'+currentDepId+': '+err);
    });

The name of the event is ViewDepartment and we send the user name and department name as properties with the event. If you check the REST request payload that is sent to MCS, you can see how the SDK is easing your life, the required context object with device information, the startSession event and the custom event itself are all included in the payload:

AnalyticsPayload

In the MCS user interface, we can navigate to the custom events analytics page, and get some nice graphs that represent our ViewDepartment event data:

AnaGraphs

Making Direct REST Calls to MCS

At the end of the day, every interaction between a client app using the MCS SDK and MCS results in a REST call being made. The MCS SDK provides a nice abstraction layer which makes your life easier and can save you a lot of time as we have seen with the payload required to send an MCS analytics event. However, there might be situations where you want to make a direct REST call to MCS, for example:

  • to call your custom API with some custom request headers
  • to get access to the raw response object returned by the REST call
  • to call a brand new REST API not yet supported by the JavaScript SDK, like the Locations API.

In such a case, the SDK can still help you by providing the base URL and the Authorization and oracle-mobile-backend-id HTTP headers. Here are two functions you can add to your mcsService to expose this data:

var getHttpHeaders = function() {
    return mbe.getHttpHeaders();
}

var getCustomApiUrl = function(customUri) {
  return mbe.getCustomCodeUrl(customUri);    
}

return {
   ...
   getHttpHeaders:getHttpHeaders,
   getCustomApiUrl:getCustomApiUrl
}

With these functions in place, it becomes quite easy to call an MCS REST API using the Angular $http service object. Here is an example that calls the /departments endpoint in our custom HR API:

$http({method:'GET'
      ,url:mcsService.getCustomApiUrl("hr/departments")
      ,headers:mcsService.getHttpHeaders()})
.then(function(response){ 
    $scope.departments = response.data; })
.catch(function(err) {
     console.log('Error calling endpoint /departments: '+err);
 });

Conclusion

The MCS Javascript Cordova SDK provides an easy and fast way to connect to your custom API’s defined in MCS as well as the various MCS platform services. In this article we provided tips and guidelines for using the MCS authorization, storage and analytics services in the context of an Angular/Ionic app. If you are using another JavaScript toolkit, most of the guidelines and code samples (with some minor tweaks) should still be useful. If you are using Oracle JET, we provide some Oracle JET specific guidelines.

In follow-up articles we will dive into push notifications and offline sync. Keep an eye on Oracle A-Team Chronicles home page, or if you are specifically interested in articles around Oracle MCS, there is also a list of MCS-related articles.

 

Add Your Comment