Technical Articles
SAPUI5: A generic promisify for sap.ui.model.odata.v2.ODataModel
These days Promises or more precisely Promises/A+ have become the standard for handling and orchestrating asynchronous operations for Web Developers using JavaScript. And with async…await on the horizon things will even get more exciting.
If you are not familiar with the term yet you can find a short introduction here.
But in this post we will not cover Promises itself but introduce a generic approach to convert ‘legacy’ Callback based APIs into ‘modern’ Promise based APIs. Under Web Developers this approach is commonly known as Promisification or short promisfy (e.g. Node.js provides it’s own util.promisify)
In particular we are going to convert methods of sap.ui.model.odata.v2.ODataModel to return proper Promises to make our lives easier.
Let’s jump right in and take a look a at typical call to sap.ui.model.odata.v2.ODataModel#read
sap.ui.define([
"sap/ui/core/mvc/Controller"
], function(Controller) {
"use strict";
return Controller.extend("blogs.sap.com.Controller", {
readData: function() {
var that = this;
that.getModel().read("/DataSet", {
success: function(oData, oResponse) {
// we got ourselves some data
that.doSthWithTheData(oData);
},
error: function(oError) {
// something went terribly wrong
that.handleTheError(oError);
}
});
}
});
});
If we want readData to return a Promise we can simply wrap the call and resolve on success and reject on error:
sap.ui.define([
"sap/ui/core/mvc/Controller"
], function(Controller) {
"use strict";
return Controller.extend("com.sap.blogs.Controller", {
readData: function() {
var that = this;
return new Promise(function(fnResolve, fnReject) {
that.getModel().read("/DataSet", {
success: function(oData, oResponse) {
// we got ourselves some data
fnResolve({
data: oData,
response: oResponse
});
},
error: function(oError) {
// something went terribly wrong
fnReject(new Error(oError.message));
}
});
})
}
});
});
In the success case we simply collect the data and response and resolve them in a single object.
For the error case we reject with a proper Error instance forwarding the message, which is best practice. Think of the error in a try-catch: catching a string or any other type would be weird, right?
We also want to have a proper stack trace in case of any error.
The above pattern can be easily applied to any calls of create, read, update, delete or callFunction of sap.ui.model.odata.v2.ODataModel.
As we are all lazy, why not create a little generic helper just like this:
sap.ui.define([], function () {
"use strict";
function _promisify(oModel, sMethod, iParametersIndex) {
return function () {
var aArguments = [].slice.call(arguments);
return new Promise(function (fnResolve, fnReject) {
var mParameters = aArguments[iParametersIndex] || {};
aArguments[iParametersIndex] = Object.assign(mParameters, {
success: function (oData, oResponse) {
fnResolve({
data: oData,
response: oResponse
});
},
error: function (oError) {
fnReject(new Error(oError.message));
}
});
oModel[sMethod].apply(oModel, aArguments);
});
};
}
return function promisify(oModel) {
return {
create: _promisify(oModel, "create", 2),
read: _promisify(oModel, "read", 1),
update: _promisify(oModel, "update", 2),
remove: _promisify(oModel, "update", 1),
callFunction: _promisify(oModel, "callFunction", 1)
};
};
});
Don’t get scared by the private _promisify method yet. Let’s start by looking at the code below.
Our little helper will export a function promisify which expects an instance of sap.ui.model.odata.v2.ODataModel (oModel) as the only argument.
Each call to this function will then return a single object which has exactly five functions: create, read, update, delete and callFunction. Each call to any of these functions will accept the same number and type of arguments as the equivalents of sap.ui.model.odata.v2.ODataModel. But with one difference: they will return a Promise.
The private helper method is the only part which is a little bit tricky.
It accepts three arguments (our oModel instance, the method name to be called and an index) and will return a function.
Whereas the first two arguments are pretty straight forward, let’s explain why we need that index.
Different to our initial example we need to know at which index to pass the mParameters argument with our success/error handlers for resolving/rejecting the Promise.
For oModel.read it will be the second argument (index 1) and for oModel.create it will be the third argument (index 2).
Once we know the index we can get the mParameters (or fallback to an empty object) and assign the success and error callbacks just as we have already seen it.
Another point worth mentioning is that we do not alter and modify the original oModel instance. So we do not need to worry about breaking anything. We are just using it internally.
After importing our promisify util we can simply start using it in our controller:
sap.ui.define([
"sap/ui/core/mvc/Controller",
"sap/base/Log",
"com/sap/blogs/promisify"
], function(Controller, Log, promisify) {
"use strict";
return Controller.extend("com.sap.blogs.Controller", {
onInit: function() {
this._oModel = promisify(this.getModel());
},
onExit: function() {
this._oModel = null;
delete this._oModel;
},
usePromisify: function() {
var that = this;
Promise
.all([
//create
that._oModel.create("/DataSet", {
some: "data"
}),
// read
that._oModel.read("/DataSet"),
// update
that._oModel.update("/DataSet(guid'x-x-x-x-x')", {
some: "update"
})
])
.then(function(aResults) {
// delete
return that._oModel.delete("/DataSet(guid'x-x-x-x-x')");
})
.then(function(oResult) {
// callFunction
return that._oModel.callFunction("/Function");
})
.catch(function(oError) {
Log.error(oError.stack);
});
}
});
});
Happy Coding!
That’s a very good way to implement promises for the odata service.. I might use this in my current project ?
Thanks,
Mahesh
Who knew? JavaScript is growing up. Nice blog.
Very nice, great reusable stuff.
Thanks for sharing. This could come in very handy 🙂
Great idea and thanks for sharing! 🙂
Thanks for sharing. A nice utility file.