Javascript Extensibility example: zip / unzip library integration
This documentation shows how to use extensibility scenarios that are experimentally deployed
in update 9. The APIs described here are minimal and will evolve in the future. They are
manually deployed, which currently prevents installation in the Cloud. They are usable only in
an Early adopter program context.
This article explains how to use asynchronous 'node.js' API in JavaScript bundles.
This article shows how to implement a bundle that allows 4GL code to call JavaScript zip and
unzip functions.
Creating our zlib bundle
As explained in the previous article the first step is to create a bundle for the extension. This
bundle has the following structure:
xa1-zlib/
package.json
lib/
zlib-helper._js
The package.json file contains:
CODECODE CODE json
"name": "xa1-zlib",
"description": "Crypto helper for Sage X3 4GL",
"version": "1.0.0",
"author": "ACME Corp.",
"private": true,
"sage": {
"x3": {
"extensions": {
"4gl-apis": [{
"module": "./lib/zlib-helper"
}]
The 4gl-apis extension key indicates that the package contains a 4GL API extension. The
module gives the relative path to the JavaScript modules that implements the extension.
Asynchronous JavaScript
Notice that our zlib-helper._js JavaScript file has a ._js extension
instead of the usual .js extension.
This is because 'node.js' makes heavy use of asynchronous APIs and SAFE X3 uses a special tool
called streamline.js for asynchronous programming. This tool uses the ._js extension in place of
the usual .js extension.
In 'node.js', any function that performs I/O (reads or write files, communicates over a network)
must be asynchronous. Asynchronous functions do not return their result directly, as a normal
synchronous function; instead, they return it through a callback. Here is a typical example:
CODECODE CODE javascript
var fs = require('fs');
console.log("before read");
fs.readFile("spleen.txt", "utf8", function(err, text) {
console.log("read completed");
if (err) throw new Error(err); // readFile failed
console.log("file contents = " + text)
// do something with text
});
console.log("read in progress ...");
This program will print:
before read
read in progress
read completed
file contents = Quand le ciel bas et lourd pèse comme un couvercle ...
As this little example demonstrates, callbacks impose a special structure to the code. It often
forces the developer to decompose its processes into very small functions and the order in
which these functions are called can be surprising at first (the fact that "read in progress ..." is
printed before "read completed" in the example above).
Streamline.js was designed to ease asynchronous programming by making it look like
synchronous code. The example above becomes:
CODECODE CODE javascript
var fs = require('streamline-fs');
console.log("before read");
var text = fs.readFile("spleen.txt", "utf8", _);
console.log("read completed");
console.log("file contents = " + text)
// do something with text
The callback has been replaced by _ and the result of the contents of the file is now returned
by
the fs.readFile call, as if this call were synchronous.
'Streamline.js' is a public Open Source project. You can find documentation on its GitHub
README page, as well as tutorial and a FAQ.
Writing our zlib helper
Next, you can create our zlib-helper._js JavaScript module. Here is the code:
CODECODE CODE javascript
var zlib = require('zlib');
exports.zip(text, _) {
// our input is text - convert it to a node.js Buffer
var buf = new Buffer(text, 'utf8');
// buf is a node.js Buffer, zip it (asynchronously)
var zipped = zlib.zip(buf, ~_);
// zipped is a node.js Buffer, return it
return zipped;
exports.unzip(data, _) {
// data is a node.js Buffer, unzip it (asynchronously)
var unzipped = zlib.unzip(buf, ~_);
// unzipped is a node.js Buffer, convert it to text
var text = unzipped.toString('utf8');
return text;
Note: SAFE X3 uses 'streamline.js' in fast mode. This option produces more efficient code, but
it requires a bit of extra care when calling asynchronous functions. The rule is the following:
If you call an asynchronous function that is implemented with 'streamline.js', you must pass _
as callback.
If you call an asynchronous function that belongs to the core 'node.js' API, or to a third party
package that was not implemented with 'streamline.js', then you must pass ~_ as callback.
In the example above, zlib.zip and zlib.unzip are native 'node.js' APIs. So you call them with ~_.
Deploying the extension on the 'node.js' web server
Deployment is easy: you just need to add your extension directory under the node_modules
directory of the SAFE X3 'node.js' server, and restart the 'node.js' server.
Do not forget to restart the 'node.js' server everytime you make a change to your bundle.
Otherwise it will not pick up the latest code.
Calling asynchronous JavaScript functions from 4GL
Now you can write a small 4GL API that delegates its work to these asynchronous functions:
Funprog ZIP(TEXT)
Clbfile TEXT
Local Integer STATUSCODE
Local Clbfile RESHEAD
Local Clbfile RESBODY
STATUSCODE = func ASYRWEBSER.EXEC_JS(
"xa1-zlib/lib/zlib-helper", # MODULE
"zip", # FUNCTION
"wait", # MODE: asynchronous, wait for result
'"' + escjson(TEXT) + '"', # ARGUMENTS
"0", # ENCODINGS: input is not encoded
-1, # CALLBACK: at the end of argument list
"", # RETURNS: empty
"1", # RETURNS_ENC: result is encoded in base 64
RESHEAD, # RESHEAD: unused
RESBODY) # RESBODY: the result, as a base 64 string
If STATUSCODE<>200 : End "" : Endif
# RESBODY is a base 64 string, return it as a Blbfile
End fromBase64(RESBODY)
Funprog UNZIP(DATA)
Blbfile DATA
Local Integer STATUSCODE
Local Clbfile RESHEAD
Local Clbfile RESBODY
STATUSCODE = func ASYRWEBSER.EXEC_JS(
"xa1-zlib/lib/zlib-helper", # MODULE
"unzip", # FUNCTION
"wait", # MODE: asynchronous, wait for result
'"' + toBase64(DATA) + '"', # ARGUMENTS: DATA as a base 64 string
"1", # ENCODINGS: input is encoded
-1, # CALLBACK: at the end of argument list
"", # RETURNS: empty
"0", # RETURNS_ENC: result is not encoded
RESHEAD, # RESHEAD: unused
RESBODY) # RESBODY: the result, as a string
If STATUSCODE<>200 : End "" : Endif
End RESBODY
Now you can use this 4GL API to zip and unzip texts:
Subprog TESTZIP()
Local Blbfile ZIPPED
Local Clbfile UNZIPPED
ZIPPED = func ZIP("Hello world!")
UNZIPPED = func UNZIP(ZIPPED);
Infbox UNZIPPED : # Should be "Hello world!"
End
Links
ASYRWEBSER API Guide
node.js API
streamline.js