This page explains how to migrate XSJS to Async-XSJS to benefit from latest Node.js versions (16 and later).
SAP HANA XSJS applications are based on synchronous API. On the other hand, Node.js is an asynchronous runtime. Thus, for XSJS applications that run on Node.js there is a certain code incompatibility. Up until Node.js 14, this incompatibility has been handled by an NPM package called @sap/fibers (forked from laverdet/node-fibers). Unfortunately, @sap/fibers is not compatible with Node.js version 16 and higher.
Also, Node.js supports two kinds of scripts:
-
CommonJS - it's synchronous and does not support top-level await. In XSJS, all files are loaded as common Java scripts.
-
ECMAScript - it's fully asynchronous and supports top-level await
The only possible way for your XSJS applications to be up and running on latest Node.js versions is to modify their codes – by migrating them to a new XSJS layer with asynchronous API. See the migration process below to learn what you need to do.
In exceptional cases (if you haven’t completed the migration to Async-XSJS), to avoid application failures during redeployment, you may pin the last buildpack version that contains Node.js14, as provided by the nodejs-buildpack community. You can do this in your manifest.yml file, and then redeploy your app. To learn how, see: Specify a buildpack version in manifest.yml
Please be advised, that SAP does not recommended usage of Node.js 14, as no support and security fixes are provided for this version anymore.
-
To switch from XSJS to Async-XSJS, open your
package.json
file, and in the dependencies section, add the @sap/async-xsjs package. For example:{ "name": "myapp", "engines": { "node": "16.x.x" }, ... "dependencies": { "express": "^4.18.1", "@sap/xssec" : "^3.2.15", "@sap/async-xsjs": "^1.0.0" } ... }
-
Enable the ECMAScript virtual-machine interface on application startup.
With @sap/async-xsjs, your asynchronous JavaScript code is loaded by default as ECMAScript (ES). To make use of the support that Node.js provides for ES modules, you need to start your application using the option --experimental-vm-modules.
{ "name": "myapp", "engines": { "node": "16.x.x" }, ... "dependencies": { "express": "^4.18.1", "@sap/xssec" : "^3.2.15", "@sap/async-xsjs": "^1.0.0" } "scripts": { "start": "node --experimental-vm-modules index.js" ... }, ... }
-
In all your
.xsjs
and.xsjslib
files, for every asynchronous function call, add an await statement. -
Every function that uses an await statement must be declared as async.
-
XSJS files should be loaded as ECMAScript. To do this, in the end of every function in an
.xsjslib
file, add an export statement.You can do this for
.xsjs
files too, but that would be only necessary if a function needs to be exported for some reason (like job execution or job handling). -
If you're using the @sap/audit-logging package in your XSJS application, the code must be migrated. For example, if you have a code snippet like this:
auditLog .securityMessage('Content of the message') .by($.session.getUsername()) .sync.log();
it should be converted to:
await auditLog .securityMessage('Content of the message') .by($.session.getUsername()) .log();
To do these steps, refer to the API of the new @sap/async-xsjs package. See: SAP HANA 2 - ASYNC XS JavaScript API Reference
You can find this API reference if you go to the SAP HANA Platform product page, and then choose View All → SAP HANA Asynchronous XS JavaScript API Reference.
For huge application codes, you can optimize your work by using a migration tool. See: @sap/async-migrator
Bear in mind that this is an open-source product and a fully-covered migration of your XSJS code is not guaranteed.
The following exemplary code snippet from an .xsjslib
file:
Original Code
... $.import('lib', 'converters'); function select(tableName){ let conn; try { conn = $.hdb.getConnection(); let resultSet = conn.executeQuery('SELECT * FROM "' + tableName + '"'); return $.lib.converters.resultSet_to_Entities(resultSet, ["id", "name"]); } finally { if (conn) { conn.commit(); conn.close(); } } }
should be converted to:
Modified Code
... await $.import('lib', 'converters'); async function select(tableName){ let conn; try { conn = await $.hdb.getConnection(); let resultSet = await conn.executeQuery('SELECT * FROM "' + tableName + '"'); return $.lib.converters.resultSet_to_Entities(resultSet, ["id", "name"]); } finally { if (conn) { await conn.commit(); conn.close(); } } } export default {select};
Since the @sap/fibrous package is not supported anymore, the .sync statements will no longer work. To fix a code that uses the sync
property, see step 4 from the procedure above.
Since all .xsjs
and .xsjslib
files are based on ECMAScript module, this pointer behaves differently from when used with CommonJS. For example, when ECMAScript is loaded, the value of this is undefined.
In the context of a module function execution, the value of pointer this is the export <object> module.
The following sample code will throw an error due to this=undefined
during loading.
=== my.xsjslib ===XSJS ====
this.funcA = function(){} (function(self){ self.funcB = function(){}; })(this); this.x = 5;
Adapt the code to Async-XSJS:
=== my.xsjslib ===Async-XSJS ====
function funcA(){} function funcB(){} var x = 5; export default {funcA, funcB, x};
This example just illustrates the problem and gives a possible solution. Depending on the software design of your application, there could be various solutions.
To learn more about the differences between these two kinds of Node.js scripts, see:
In the Async-XSJS layer, the Array.forEach and Array.map functions change their behavior when the argument function is asynchronous.
For example, the SQL statements are read and executed sequentially, one after the other, and then written to the database in the same order.
=== my.xsjslib ===XSJS ===
var sqls = ['create table "PRODUCT"("ID" integer, "NAME": varchar(200))', 'insert into "PRODUCT" values (1, \'iphone\')' ]; function runSql(sql){ var conn = $.hdb.getConnection(); conn.execute(sql); } sqls.forEach(runSql); ... <additional code>
The following example shows how the same code looks when adapted to Async-XSJS:
=== my.xsjslib ===Async-XSJS ===
var sqls = ['create table "PRODUCT"("ID" integer, "NAME": varchar(200))', 'insert into "PRODUCT" values (1, \'iphone\')' ]; async function runSql(sql){ var conn = await $.hdb.getConnection(); await conn.execute(sql); } sqls.forEach(runSql); ... <some additional code>
In the previous example, the SQL statements are only scheduled for execution; they are not executed immediately. When it is time to record them in the database, the order of operation might be different to the order in which the tables were initially read.
If you have additional code after the sqls.forEach function, it will be executed before any of the SQL statements.
To avoid unwanted side effects and ensure that the SQL statements are created in the database in the correct order, you can replace sqls.forEach(runSql); with one of the following examples:
-
Sequential execution
for (int i=0; i<sqls.length; i++){ await runSql(sqls[i]); } <some additional code> //SQL statements are executed in the correct order, then the additional code is executed
-
Parallel execution
await Promise.all(sqls.map(runSql)); <some additional code> //SQL statements are executed in a random order, then the additional code is executed
The same configuration steps can be applied for Array.map
in analogical way – just replace sqls.forEach
with sqls.map
.
Another function that can be asynchronous is Array.reduce
.