While I am working with NodeJS on the IBM i, what came to my mind was the question:
"Can I call an ILE subprocedure with the IBM i port of NodeJS?"
So far, the answer is "No." But, I though since the world seems to be heading in the direction of using Web Services for pretty much everything, why not create web services for specific ILE functions that are then called from NodeJS?
The first example I set up was to send a simple email using the ILE functions included with MAILTOOL, a utility that lest you send email from your IBM i using a command or an ILE interface. The full working demo can be found here. If for some reason it's not working, send me an email using the BVSTools Contact page and I'll make sure the instance is up and running.
First was setting up a NodeJs route named mailtoolDemo.js as shown here:
var express = require('express');
var router = express.Router();
router.all('/', function(req, res, next) {
res.render('mailtoolDemo');
});
module.exports = router;
The next thing I did was create a view named mailtoolDemo.jade (yes, I'm using Jade as my template engine currently for NodeJS development).
extends layout
block content
h2 MAILTOOL Demo using NodeJS and Web Services
form(name="mailtool", id="mailtoolDemoForm" action="/sendMail", method="post")
p
| To Address:
p
input(type="text", name="toAddress", maxLength=256)
p
| Subject:
p
input(type="text", name="subject", maxLength=1024)
p
| Message:
p
textarea(type="text", name="message", maxLength=5000)
p
input(type="submit", value="Send")
#demoResults
As we can see, the form will call a nodeJS application named sendMail. But in our case, instead of going to a different screen to display the results, we decided to use some jQuery to return the results of the call to the DIV container named demoResults on the web page. You can choose to skip this if you want and things should work fine. But, in case you're interested, the jQuery source looks like this:
$(document).ready(function () {
//other stuff
mailtoolDemoForm();
});
function mailtoolDemoForm() {
$("body").on('submit', '#mailtoolDemoForm', function (event) {
event.preventDefault();
var $form = $(this);
var $inputs = $form.find("input, select, button, textarea");
var serializedData = $form.serialize();
$inputs.prop("disabled", true);
ajaxMessage = "Sending email...";
var request = doAjax($form.attr('action'), serializedData);
request.done(function (response, textStatus, jqXHR) {
$( "#demoResults" ).html( response );
});
request.fail(function (jqXHR, textStatus, errorThrown) {
ajaxMessage = "Error sending email: " + textStatus, errorThrown;
$( "#demoResults" ).html( ajaxMessage );
console.log(ajaxMessage);
});
request.always(function () {
$inputs.prop("disabled", false);
$.unblockUI();
});
});
}
What this does is use AJAX to call the /sendMail NodeJS function instead of a direct POST from the web page. There are a few other proprietary functions used in this jQuery JavaScript (such as doAjax()) which I am happy to provide if needed.
The /sendMail NodeJS route looks like the following:
var express = require('express');
var router = express.Router();
var http = require('http');
router.post('/', function(req, res, next) {
var toAddress = req.body.toAddress;
var subject = req.body.subject;
var message = req.body.message;
var data = JSON.stringify({toAddress:toAddress, subject:subject, message:message});
var options = {
host: 'ws.bvstools.com',
port: '80',
path: '/webservice/sendmail',
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded '
}
};
var req = http.request(options, function(httpReq) {
var msg = '';
httpReq.setEncoding('utf8');
httpReq.on('data', function(chunk) {
msg += chunk;
});
httpReq.on('end', function() {
var jsonContent;
var success;
var response;
try {
jsonContent = JSON.parse(msg);
success=jsonContent.success;
response=jsonContent.response;
} catch(e) {
success='false';
response='Error parsing response: ' + e;
}
res.render('sendMail', { success: success, response: response });
});
});
req.write(data);
req.end();
});
module.exports = router;
What you'll notice in this is that we do a POST using the http request method. This is similar to making a web service request using cURL or GETURI. you may notice the content type we are using is application/x-www-form-urlencoded vs application/json. That's because we wanted to make this simple. The HTTP server on the IBM i won't convert application/json data automatically to our jobs CCSID as it is, so we thought we'd keep the RPG CGI portion of this as familiar as possible.
When we make the post we're creating JSON to pass along as the POST data to the RPG CGI (eRPG) program named SENDMAIL. This way when the data is read in we can parse the data using the RPG port of the YAJL JSON parser.
The Jade view for this route is again very simple:
block content
p
|success: #{success}
p
|response: #{response}
Now comes the good part.. the RPG CGI program that will process the data, send the email, and return a response (in JSON) to the application.
H DFTACTGRP(*NO) BNDDIR('ERPGSDK')
****************************************************************
* Prototypes *
****************************************************************
/COPY QCOPYSRC,P.ERPGSDK
/COPY QCOPYSRC,P.HTTPSTD
/COPY QCOPYSRC,P.LIBL
/COPY QCOPYSRC,P.MAILTOOL
/COPY QCOPYSRC,yajl_h
*
D #RdStinPtr PR ExtProc('QtmhRdStin')
D RcvRec * Value
D RcvLen 10I 0 Const
D RcvRecLen 10I 0
D WPError 56
****************************************************************
* Data read in from page
D data@ S *
*
D WPError DS
D EBytesP 10i 0 INZ(%size(WPError))
D EBytesA 10i 0
D EMsgID 7
D EReserverd 1
D EData 40
*
D docNode s like(yajl_val)
D val s like(yajl_val)
*
D contentLength S 10i 0
D stdinLength S 10i 0
D toAddress S 65535 Varying
D subject S 65535 Varying
D message S 65535 Varying
*
D rc S 10i 0
D rtnLen S 10i 0
D msgID S 13s 0
D errorMsg S 256
D response S 256
D yajl_err S 500 Varying
****************************************************************
/free
Monitor;
contentLength = %dec(#GetEnv('CONTENT_LENGTH'):10:0);
On-Error;
contentLength = 0;
EndMon;
if (contentLength <= 0);
errorMsg = 'No data - Content length=' + %char(contentLength) + '.';
exsr $Return;
endif;
data@ = %alloc(contentLength);
#RdStinPtr(data@:contentLength:stdinLength:WPError);
docNode = yajl_buf_load_tree(data@:stdinLength:yajl_err);
if (docNode = *NULL);
errorMsg = 'Error loading docNode: ' + %trimr(yajl_err);
exsr $Return;
endif;
val = YAJL_object_find(docNode:'toAddress');
toAddress = yajl_get_string(val);
val = YAJL_object_find(docNode:'subject');
subject = yajl_get_string(val);
val = YAJL_object_find(docNode:'message');
message = yajl_get_string(val);
yajl_tree_free(docNode);
if (toAddress = ' ');
errorMsg = 'To Address cannot be blank.';
exsr $Return;
endif;
exsr $MailTool;
exsr $Return;
//***************************************************************
//* Call MAILTOOL
//***************************************************************
BegSr $MailTool;
#pushLib('MAILTOOL');
#pushLib('G4GBVS');
subject = '[MAILTOOL nodeJS Demo]: ' + subject;
if (#mailtool_init() >= 0);
rc = #mailtool_setValue('configuration_file':
'/bvstools/bvstools_mailtool.json');
rc = #mailtool_loadDefaults();
rc = #mailtool_addTORecipient(toAddress:errorMsg);
rc = #mailtool_setValue('subject':subject);
rc = #mailtool_setValue('message':message);
rc = #mailtool_sendMail(errorMsg:msgID);
if (rc < 0);
errorMsg = 'Error sending email: ' + %trimr(errorMsg);
endif;
endif;
#popLib('MAILTOOL');
#popLib('G4GBVS');
EndSr;
//-------------------------------------------------------------/
// Return /
//-------------------------------------------------------------/
begsr $Return;
if (docNode <> *NULL);
yajl_tree_free(docNode);
endif;
if (data@ <> *NULL);
dealloc data@;
endif;
#writeTemplate('stdhtmlheader.erpg');
#loadTemplate('message.erpg');
yajl_genOpen(*OFF); // use *ON for easier to read JSON
// *OFF for more compact JSON
yajl_beginObj(); //main JSON object
if (errorMsg <> ' ');
yajl_addChar('success':'false');
yajl_addChar('response':%trimr(errorMsg));
else;
yajl_addChar('success':'true');
yajl_addChar('response':'Email sent. ID:' + %char(msgID));
endif;
yajl_endObj();
rc = yajl_copyBuf(0:%addr(response):%size(response):rtnLen);
yajl_genClose();
#replaceData('/%msg%/':response);
#writeSection();
#cleanup();
*INLR = *on;
return;
endsr;
/end-free
First we read the CONTENT-LENGTH header to see how big the JSON data we're sent is.
Next we allocate memory to hold the data read in from the POST. We then parse the JSON, do some error checking and if things are ok, we send the email. There are of course many more parameters we could be setting but to keep this simple we stuck with the basics of the To address, Subject and Message.
We return a set of JSON data with a success and response message. This is then parsed by the processing program and display on the web page for the results.
So, while we don't have a direct ILE interface with NodeJS if we use the ever more popular method of web services we can get things done.
I've mentioned in the past I have a love/hate relationship with NodeJS. That is because I love programming in JavaScript, but I also think things are done easier with less "spaghetti" and little modules with RPG. But, as they say, nothing is for free.
"Can I call an ILE subprocedure with the IBM i port of NodeJS?"
Can you clarify why this didn't work for you Brad? It was my understanding that XMLSERVICE was the underlying tool used for the call mechanism and it can call subprocedures. Is it just that the Node.JS interface doesn't surface the capability?
Hi, Jon.
I know I can use XMLSERVICE to call using stored procedures or something else (Aaron and I discussed this on LI) but I took a different more generic route of using Web services.
This way the caller and the callee don't have to have any proprietary features. A web service can be called from virtually any interface, and a web service can be written with virtually any interface.
I also wanted to learn how to call a web service using Node.js. It's a little clunky now, but I'm sure it will get better and easier like it is with jQuery as things progress. (Man, all the jQuery features in nodeJS would be the bees knees!)
Please feel free to post an example of something you've used that worked with XMLSERVICE or another method. I'd be interested in more than the generic examples in the IBM node documentation and see some real world examples.
Not talking about stored procs Brad. And I understand and agree that a web service is a more generic way to go.
But ... You specifically said that you couldn't call a subprocedure with the toolkit and I was just wondering why you thought that.
Directly call an ILE subprocedure from Node.JS? Can you post an example or provide a link to one?