Índice de contenidos
Problema afrontado
Se solicita que, desde Sage X3, exista la posibilidad de generar etiquetas que contengan códigos QR para identificar artículos a través de un escáner.
Solución propuesta
La solución que se propone es la siguiente: externalizar la funcionalidad de manera que se realice en un paquete o bundle programado en Node JS, tecnología que optimiza los tiempos de desarrollo y facilita el trabajo gracias a su amplia comunidad y versatilidad.
Estructura de ficheros del bundle
Los ficheros están comprendidos en un directorio cuyo nombre va a ser el mismo de nuestro módulo, al incorporar sólo uno. En este ejemplo es “xeax-qr-generator”.
Dentro se encuentra el directorio “lib”, donde va a ir todo el código que se vaya a ejecutar en el lado del servidor.
- xeax-qr-generator
- lib
- services
- eax-qr-generator-service._js
- index._js
- services
- package.json
- lib
Cómo se comunican 4GL y NodeJS
La manera en la que 4GL y Node JS se comunican es a través de la API de “ASYRWEBSER”, que permite ejecutar las funciones de Javascript que se publican en los módulos pertenecientes a los bundles, mediante la configuración del archivo package.json.
Cómo configurar el fichero “package.json”
Para configurar el fichero “package.json” hay que tener en cuenta una serie de propiedades obligatorias, que son:
- “name”: Es importante especificar el nombre del módulo, pues va a ser la manera de identificarlo desde 4GL.
- “module”: También es necesario especificar el fichero donde se encuentran las funciones a exponer. En este caso es el fichero “index._js”.
Nótese que se debe mantener la estructura de árbol sage > x3 > extensions > 4gl-apis >module.
- “dependencies”: Esta propiedad alberga las dependencias que emplea el bundle. En este caso se emplea la librería qrcode con la versión desde 1.4.4 hasta la 2.0.0.
{ "name": "xeax-qr-generator", "version": "1.0.0", "description": "Emiral's Sage QR generator", "author": "Emiral Freelance Group", "sage": { "x3": { "extensions": { "4gl-apis": [ { "module": "./lib/index" } ] } } }, "dependencies": { "qrcode": "^1.4.4" } }
xeax-qr-generator/package.json
Exponiendo la función
El fichero donde se publican las funciones a ejecutar desde 4GL es el “index._js”, de manera que se deben exportar:
"use strict"; /** * Endpoint called by 4GL to generate QR codes. * @param _ - This param is automatically filled and it's a callback reserved for async functionality * @param strData - Parameters to send */ exports.generateQR = function (_, qrParams) { let res = ""; try { const EAXQRGeneratorService = requireUncached("./services/eax-qr-generator-service"); const qrGeneratorSvc = new EAXQRGeneratorService(); // Generating QR code res = qrGeneratorSvc.generateQR(_, qrParams); } catch (error) { console.log(error); res = JSON.parse(error); } return res; }; /** * Does the sames as require function forcing to read the file everytime it's accessed. * @param module * @returns */ function requireUncached(module) { delete require.cache[require.resolve(module)] return require(module) }
index._js
Los ficheros acabados con la extensión _js están destinados a ser procesados por la librería streamline.js, que es empleada por Syracuse por lo simple que hace el manejo del código asíncrono. Como las peticiones HTTP o la lectura de ficheros, la generación de QR no es inmediata; es por eso que se trabaja de forma asíncrona.
Se exporta la función generateQR, la cual recibe dos parámetros:
- El primero es la función callback empleada por la librería streamline.js, que se debe pasar por parámetro a las funciones que lo admitan (como el then de las promesas). De esta manera, cuando se obtenga el resultado, 4GL será notificado.
- “qrParams”: El JSON que contiene la configuración deseada para la generación del código QR.
Lo que hace la función es delegar en el servicio llamado “” la funcionalidad de generación de QR.
Agilizando el desarrollo
Se ha separado la funcionalidad del fichero de entrada “index” por cuestiones de optimización de tiempos y comodidad a la hora del desarrollo. Al aislarse, el programador puede hacer una lectura del fichero desde su ubicación y no desde la caché.
Todos los paquetes de Node JS son leídos y almacenados en memoria al arrancar el servidor de Syracuse. Para realizar lecturas desde el sistema de archivos se puede forzar eliminando el dato almacenado en memoria, como se puede ver en la función “requireUncached”, que una vez eliminado de la caché, y llamar a la función “require” está forzando al servidor a que lo vaya a buscar en la ruta física.
Este tipo de lectura debería dejar de hacerse en los despliegues en entornos de producción por cuestiones de rendimiento.
Generación de códigos QR
Como ya se ha mencionado antes, se está haciendo uso de la librería de qrcode. Esta librería permite configurar la manera en que se genera de una forma sencilla.
En este ejemplo se ha creado un formato concreto de los parámetros para cubrir las necesidades del desarrollo:
- “strData”: Contenido a codificar.
- “quality”: Calidad de la imagen a generar (definidos en la página de la librería). Por defecto es alta (“H” de “high”).
- “width”: Tamaño del lado (alto/ancho) de la imagen a generar.
- “scale”: Densidad de píxeles por módulo (la unidad atómica que conforma el código QR).
Se importa la librería en cuestión y gracias a la función “toBuffer” podemos realizar la generación asíncrona del código QR. Para obtener el resultado es importante recogerlo en una variable y situar a la derecha del igual la función que genera la promesa, seguida de “.then(_,_)”, que hace que a la hora de programar se haga de una manera parecida a la síncrona (parecido al no tan moderno async/await). Como ya se ha mencionado antes, esto es gracias a la librería de streamline.js.
Una vez se ha obtenido el buffer, se codifica en base64 para ser procesado por 4GL.
module.exports = class EAXQRGeneratorService { constructor() { } generateQR(_, qrParams) { const strData = qrParams.strData; const quality = qrParams.quality || 'H'; const width = qrParams.width; const scale = qrParams.scale; var QRCode = require('qrcode'); let qrBuffer = QRCode.toBuffer(strData, { errorCorrectionLevel: quality, width: width, scale: scale }).then(_, _); const result = qrBuffer.toString('base64'); return result; } }
Ejecución de función JavaScript desde 4GL
Para este ejemplo se ha desarrollado un script programado en 4GL.
Primero se almacenan los parámetros a pasar en una variable de tipo clob en formato JSON.
Se ejecuta la función EXEC_JS de la api ASYRWEBSER pasándole los siguientes parámetros:
- Módulo (MODULE): Ruta entera desde el nombre del paquete hasta el fichero de entrada.
- Función (FONCTION): Función deseada, publicada en el módulo o archivo expuesto (index).
- Modo (MODE): Para ejecutar de manera asíncrona y que X3 incruste el callback en los parámetros de la función publicada.
- Parámetros (DATA): Parámetros a pasar a la función publicada.
- Entrada base 64 (B64_IN): No aplica, pasar “0”
- Posición callback (CALLB): Indica la posición en los parámetros donde inyectar la función callback. En el ejemplo es 0 para que se pase como primer parámetro.
- Devolución (RETURNS): Empleado para especificar la propiedad del objeto a extraer. En este caso al no ser un objeto JS, sino un string, no se necesita, pasar “”.
- Salida base 64 (B64_OUT): No aplica, pasar “0”.
- Cabeceras de respuesta (RESHEAD): No aplica, dejar en blanco.
- Cuerpo de respuesta (RESBODY): Variable que se llena con el resultado emitido por la función del bundle. En este ejemplo contendrá el código en base64 de la imagen asociada al texto que se le pasó en los parámetros “Texto de prueba”.
Local Clbfile DATA DATA = '{' Append DATA, '"strData": "Texto de prueba"' # QR Text Append DATA, ',"quality": "H"' # L, M, Q, H - (L = Low, H = High [default]) Append DATA, ',"width": null' # Width of image Append DATA, ',"scale": null' # Number of pixels per module (4px by default) Append DATA,'}' Local Clbfile RESHEAD Local Clbfile RESBODY Local Integer STATUSCODE ### Module, function and element to be returned Local Char MODULE(100) : MODULE = 'xeax-qr-generator/lib/index' Local Char FONCTION(30) : FONCTION = 'generateQR' #'getUserProfile' Local Char RETURNS(50) : RETURNS = ""#default" ### The function is asynchronous Local Char MODE(4) : MODE = 'wait' ### The callback argument is located in the first place for this function Local Integer CALLB : CALLB = 0 ### No Base 64 in or out coding, the arguments can remain empty Local Char B64_IN(20): B64_IN = "0" Local Char B64_OUT(20): B64_OUT = "0" ### No additional arguments Local Clbfile ARGUMENTS : ARGUMENTS = DATA # Let's call the function STATUSCODE = func ASYRWEBSER.EXEC_JS(MODULE, FONCTION, MODE, DATA, B64_IN, CALLB, RETURNS, B64_OUT, RESHEAD, RESBODY) Infbox(RESBODY) End