X

Best Practices from Oracle Development's A‑Team

Creating a Mobile-Optimized REST API Using Oracle Mobile Cloud Service - Part 4

Introduction

To build functional and performant mobile apps, the back-end data services need to be optimized for mobile consumption. RESTful web services using JSON as payload format are widely considered as the best architectural choice for integration between mobile apps and back-end systems. At the same time, many existing enterprise back-end systems provide a SOAP-based web service application programming interface (API). In this article series we will discuss how Oracle Mobile Cloud Service (MCS) can be used to transform these enterprise system interfaces into a mobile-optimized REST-JSON API. This architecture layer is sometimes referred to as Mobile Backend as a Service (MBaaS). A-Team has been working on a number of projects using MCS to build this architecture layer. We will explain step-by-step how to build an MBaaS, and we will  share tips, lessons learned and best practices we discovered along the way. No prior knowledge of MCS is assumed. In part 1 we discussed the design of the REST API, in part 2 we covered the implementation of the "read" (GET) resources, in part 3 we discussed implementation of the "write" resources (POST,PUT and DELETE). In this fourth part, we will look at how we can use MCS Storage collections to cache payloads, and while doing so, we will use some more advanced concepts like chaining promises to execute multiple REST calls in sequence.

Main Article

In this article we will implement the GET /jobs endpoint which returns a list of jobs. This list is static, and as such can be cached within MCS to reduce the number of backend calls and speed up overall performance. Obviously, app developers can also choose to cache this list on the mobile device to further enhance performance, but that is beyond the scope of this article. We will use the MCS Storage API to store and retrieve the cached list of jobs. We will use a boolean query parameter refreshCache to force an update of the jobs list in storage.

Setting up the Storage Collection

To store files in MCS, a so-called storage collection must be created. Access to a storage collection is handled through roles. When creating a new storage collection, you assign roles that have read and/or write privileges. Users with the appropriate role(s) can then store files in the collection and/or retrieve them. So, we first create a role named HRManager, by clicking on the Mobile User Management menu option, select the Roles tab, and then click on New Role.

NewRole

After creating the role, we select the Storage menu option and click on New Collection to create the collection

NewCollection

We leave the collection to its default of Shared. After clicking the Create button, we assign read and write permissions to the role we just created:

CollectionPermissions

Finally, we need to associate this storage collection with our mobile backend. We open the HumanResources mobile backend, click on the Storage tab, click Select Collections, and then enter "HR" to link the collection we just created with our mobile backend.

SelectCollection

Implementing the GET Jobs Endpoint

The behavior for the GET /jobs endpoint that we need to implement is as follows:

If refreshCache query parameter is set to true:

  • Invoke the findJobsView1 SOAP method
  • Transform the SOAP response into JSON format as defined during API design (see part 1)
  • Store the JSON payload in the HR storage collection in a file named JobsList
  • Return the content of JobsList as response

If refreshCache query parameter is not set, or set to false:

  • Retrieve the JobsList JSON file from the HR collection
  • If the file is found, return the content as response
  • If the file is not found, perform the steps described above when refreshCache is true.

To keep our main hr.js file clean, we start with adding a call in this file to a new getJobs function that we must implement in hrimpl.js:

service.get('/mobile/custom/hr/jobs', function (req, res) {     hr.getJobs(req,res,(req.query.refreshCache === 'true')); });

Refer to part 2 for more info on this separation between the contract and the implementation of our human resources API. The signature of the getJobs function in hrimpl.js looks like this:

exports.getJobs = function getJobs(req, res, refreshCache) {   // TODO: add implementation } 

Implementing Helper Functions

To keep the implementation clean and readable, we will first define 3 helper functions, one for each service call we need to make. The function to invoke the SOAP web service to retrieve the list of jobs looks as follows:

function getJobsFromSOAP(sdk) {   var requestBody = {Body: {"findJobsView1": null}};    return sdk.connectors.hrsoap1.post('findJobsView1', requestBody, {inType: 'json', outType: 'json'}).then(       function (result) {           var response = result.result;           var jobList = response.Body.findJobsView1Response.result.map(transform.jobSOAP2REST);           return jobList;       },       function (error) {          throw(error.error);       }     ); }

The transformation function jobSOAP2REST in transformations.js looks like this:

exports.jobSOAP2REST = function(job) {     var jobRest = {id: job.JobId, name: job.JobTitle, minSalary: job.MinSalary, maxSalary: job.MaxSalary};     return jobRest; };

With the concepts you learned in previous parts, most of this code should be pretty self-explanatory. We use the hrsoap1 connector to invoke the findJobsView1 SOAP method. By specifying the inType and outType, we instruct MCS to auto-convert the request from JSON to XML, and the response back from XML to JSON. Also notice the return keyword in front of the connector call, by returning the connector call promise, we can chain multiple service calls, as we will explain later in more detail. Finally note that in case of an error, we use the throw function. We will explain this later on as well.

The function to store the jobs list in HR collection looks as follows:

function storeJobsInStorage(sdk,jobs) {    return sdk.storage.storeById("HR", 'JobsList', JSON.stringify(jobs), {contentType: 'application/json', mobileName: 'JobsList'}).then(          function (result) {            return JSON.stringify(jobs);          },          function (error) {            throw(error.error);          }        ); }

We use the storeById function of the MCS storage API to store the jobs list as a JSON file. The mobileName property is set to JobsList to ensure that the file ID is set to JobsList instead of a system-generated value, which makes it easier to retrieve the file from storage later on.

And here is the last helper function to retrieve the JobsList from the HR storage collection:

function getJobsFromStorage(sdk) {    return sdk.storage.getById("HR", 'JobsList').then(          function (result) {              return result.result;          },          function (error) {            throw(error.error);          }        ); }

Implementing the GetJobs Function

With these helper functions in place, we can now code our main getJobs function. Here is the full implementation:

exports.getJobs = function getJobs(req, res, refreshCache) {     var sdk = req.oracleMobile;     if (refreshCache) {         getJobsFromSOAP(sdk)           .then(function(jobs) {             return storeJobsInStorage(sdk,jobs);         }).then(function(jobs) {             res.send(200,jobs).end();         }).catch(function(error) {             res.send(500,error).end();         });     }     else {       getJobsFromStorage(sdk)         .then(function (jobs) {             res.send(200,jobs).end();       }).catch(function(error) {           var statusCode = JSON.parse(error).status;           if (statusCode===404) {             // Jobs list not yet in cache, do recursive call to get from             // SOAP service and store in cache             getJobs(req,res,true);           }           else {             // something else went wrong, just return the error             res.send(statusCode,error).end();           }       });     } };

This code nicely illustrates how we can execute multiple service calls sequentially by simply chaining the promises using then(). This is how it works:

  • By returning the promise in line 6, we can chain the next then() function, which in our case writes the response
  • The input parameter of the function passed into then() is determined by the previous promise: the getJobsFromSOAP method returns the jobs as a JSON array which is then passed into the storeJobsInStorage method. This method returns the same jobs array, so we can use it in line 8 to send the response of our GET /jobs endpoint.
  • If an unexpected error occurs, the promise function throws an error which is caught on line 9, and sent as response on line 10. When an error is thrown the chain of promises using then() is interrupted, similar to a try-catch block in Java.
  • When no cache refresh is requested the catch function in line 17 is used to determine whether the JobsList is already in the cache. The error message returned by the getById function of the storage API when the file is not present looks like this:
    {   "type": "http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.1",   "status": 404,   "title": "The Storage Service can't find the object in the collection.",   "detail": "The Storage Service can't find an object with ID JobsList in the HR collection. Verify that the object ID is correct.",   "o:ecid": "005CWENZaf4A9T3_RlXBid0004il00002l, 0:3",   "o:errorCode": "MOBILE-82701",   "o:errorPath": "/mobile/platform/storage/collections/HR/objects/JobsList" }

    This error message is thrown in our getJobsFromStorage helper function which allows us to inspect it inside the catch block in line 18.  If the value of the status attribute of the error message is 404 (Not Found), we know the file is not present in the cache, either because this is the very first time the endpoint is executed, or because someone deleted the file. If this the case, we make a recursive call to our getJobs function with the refreshCache flag set to true which will populate the cache and return the jobs list as response. Note that for the recursive call to work, we need to repeat the name of the exported function (getJobs) with the function declaration.

  • If the getById function call returns another value for the status attribute, an unexpected error occurred so we simply return the error message to enable the invoker to figure out what is wrong.

As you can see the usage of promises allows us to write clean and easily readable code, much better then deeply nested callbacks (aka as "callback hell") that we easily can get when using callback functions. In addition, we no longer need a library like the NodeJs asynch module to avoid such deeply nested callbacks.

Oracle MCS uses the bluebird library for implementing these promises. In this article, we have seen a simple example where we implemented sequential asynchronous calls by simply chaining then() functions. Bluebird also has support for more advanced scenarios, like executing multiple asynchronous calls in parallel and then doing some final processing when all calls have completed. The bluebird documentation has a useful section Coming from Other Libraries that describes these more advanced scenario's.

Testing the GET Jobs Endpoint

To test our implementation we can use Postman REST client as described in part 2:

TestGetJobs1

You can no longer use the anonymous access key in the Authorization header because the anonymous user has no access to the HR collection. You need to create a mobile user for the mobile backend, assign the HRManager role to this user, and use the credentials of this user in the Authorization header.

To verify that the JobsList is cached indeed in our HR storage collection, we can also execute the GET /mobile/platform/storage/collections/HR/objects endpoint which returns the metadata of all files in he HR collection:

TestGetJobs2

You can also use the MCS web user interface to inspect, navigate to the HR collection, and click on the Contents tab, this will show a list of all files in the collection.

Note that if you then call GET /jobs?refreshCache=true to force a refresh of the cache, you will see the that the eTag attribute is incremented to "2" when you execute the storage metadata endpoint again. The (new) value of the eTag can be inspected by client applications to check whether they need to download the content of the file again in case on-device caching was applied as well.

Conclusion

This article showed how you can use the MCS storage API to cache payloads to reduce the number of potentially slow calls to backend services. However, that was just a sample used to illustrate the key takeaway of this article: the power of the bluebird promises library used in Oracle MCS which allows you to write complex orchestrations in a clean and concise way.

Be the first to comment

Comments ( 0 )
Please enter your name.Please provide a valid email address.Please enter a comment.CAPTCHA challenge response provided was incorrect. Please try again.Captcha