Node.js sur IBM i, des webservices REST devant Db2 et le RPG
Ce qu'est Node.js, qui l'a porté sur IBM i et à quoi il sert. Puis l'installation par yum, une table Db2 for i et trois webservices : un CRUD, l'appel d'un programme RPG existant et une transaction SQL. Avec un écran de test, des tests automatiques dans Chrome et Postman, et toutes les erreurs rencontrées en route.
En une phrase
Node.js fait tourner du JavaScript côté serveur ; sur IBM i, il tourne dans PASE et parle directement à Db2 for i et aux programmes RPG : c'est une façon simple de mettre une API REST ou un écran web devant une application existante, sans la réécrire.
Node.js, vite fait
Node.js a été écrit par Ryan Dahl en 2009 et présenté à la première JSConf européenne, à Berlin. L'idée : prendre V8, le moteur JavaScript de Chrome, et lui donner accès aux fichiers et au réseau. Le gestionnaire de paquets npm est arrivé en 2010 ; le projet est aujourd'hui porté par l'OpenJS Foundation.
Sa particularité est la boucle d'événements : un seul fil d'exécution, qui ne reste jamais à attendre. Pendant que Db2 répond à une requête, Node.js traite les autres. Pour un webservice, qui passe l'essentiel de son temps à attendre la base, c'est un modèle très économe.
Les versions paires deviennent des versions à support long (LTS) : ce sont celles à mettre en production. Les versions impaires sont des versions de passage.
Qui l'a mis sur IBM i ?
IBM, le 15 décembre 2014 : Node.js arrive comme option 1 du programme sous licence 5733-OPS (IBM i Open Source Solutions), annoncé avec le TR9 de la 7.1 et le TR1 de la 7.2. Il tourne dans PASE (5770-SS1 option 33), l'environnement d'IBM i qui exécute les programmes AIX.
Le 5733-OPS a ensuite laissé la place aux paquets RPM installés par yum, comme
Python, Git ou Mapepire. En 2018, IBM a indiqué que toutes les modifications propres à IBM i étaient
dans le dépôt officiel de Node.js depuis la version 10 : IBM i est une plateforme de Node.js comme
une autre, et process.platform y vaut os400.
IBM maintient aussi les paquets npm qui font le lien avec le système : idb-connector et idb-pconnector pour Db2 en local, itoolkit pour appeler les programmes RPG, CL ou les commandes par XMLSERVICE, et le paquet odbc pour Db2 à distance.
À quoi ça sert sur IBM i
À ouvrir l'application IBM i sur l'extérieur sans toucher à ce qui marche : les données restent dans Db2, les règles dans le RPG, et Node.js fait la traduction en HTTP et en JSON.
| Besoin | Outil | Sur cette page |
|---|---|---|
| Exposer une table Db2 en REST | idb-pconnector (ou odbc) | Exemple 1 : CRUD sur NODEWS.ARTICLE |
| Réutiliser un programme RPG ou CL existant | itoolkit + XMLSERVICE | Exemple 2 : le RPG CALCDEVIS devient POST /api/devis |
| Garder la règle métier dans Db2 | idb-pconnector, CALL d'une procédure SQL | Exemple 3 : MVT_STOCK, tout ou rien |
| Un écran web pour les utilisateurs | Fichiers statiques servis par Node.js | L'écran de test, au design Carbon d'IBM |
| Appeler des API du monde extérieur | fetch, intégré à Node.js | Météo, paiement, transporteur, IA |
| Des outils d'exploitation en script | node, npm, les services SQL QSYS2 | Ce que l'on faisait en CL ou en QSH |
Face au webservice en RPG (CGI ou IWS), Node.js apporte un écosystème immense (npm), la validation des entrées, la documentation OpenAPI et les tests automatiques presque sans effort. Le RPG garde l'avantage quand toute la logique est déjà en RPG et qu'on veut un seul langage.
Le cheminement d'une requête
| Étape | Qui | Quoi (exemple 2) |
|---|---|---|
| 1 | Le navigateur ou Postman envoie | POST /api/devis avec {"code":"LIC-RDI","quantite":60} |
| 2 | Fastify valide | Le schéma JSON refuse tout corps incorrect par un 400, avant d'appeler l'IBM i |
| 3 | Node.js appelle | itoolkit écrit le XML d'appel et le passe à XMLSERVICE, dans le même travail |
| 4 | Le RPG calcule | CALCDEVIS lit l'article en SQL et rend PU, remise, HT, TVA, TTC, code retour |
| 5 | Node.js répond | Code retour RPG traduit en statut HTTP (OK = 200, INC = 404), puis JSON |
Étape 1 Installer
Installer le dernier Node.js par yum
Prérequis : l'environnement open source d'IBM i (yum) et un accès au dépôt d'IBM. Depuis une session SSH, ou ACS (Open Source Package Management), on regarde d'abord ce que propose le dépôt :
$ yum list available 'nodejs*'
nodejs14.ppc64 14.21.3-1 ibmi-base
nodejs16.ppc64 16.20.2-1 ibmi-base
nodejs18.ppc64 18.20.8-1 ibmi-base
nodejs20.ppc64 20.20.1-1 ibmi-base
nodejs22.ppc64 22.23.4-1 ibmi-base
nodejs24.ppc64 24.22.1-1 ibmi-base
La plus récente est nodejs24. Le paquet nodever permet ensuite de changer de version
par défaut si plusieurs sont installées :
$ yum -y install nodejs24 nodever
Setting up Install Process
Resolving Dependencies
--> Running transaction check
---> Package nodejs24.ppc64 0:24.22.1-1 will be installed
---> Package nodever.noarch 0:1.0.0-4 will be installed
--> Finished Dependency Resolution
Dependencies Resolved
================================================================================
Package Arch Version Repository Size
================================================================================
Installing:
nodejs24 ppc64 24.22.1-1 ibmi-base 34 M
nodever noarch 1.0.0-4 ibmi-base 9.7 k
Transaction Summary
================================================================================
Install 2 Packages
Total download size: 34 M
Installed size: 209 M
Downloading Packages:
--------------------------------------------------------------------------------
Total 321 kB/s | 34 MB 01:48
Running Transaction Check
Running Transaction Test
Transaction Test Succeeded
Running Transaction
Installing : nodever-1.0.0-4.noarch 1/2
Installing : nodejs24-24.22.1-1.ppc64 2/2
update-alternatives: using /QOpenSys/pkgs/lib/nodejs24/bin/node to provide /QOpenSys/pkgs/bin/node (node) in auto mode
Installed:
nodejs24.ppc64 0:24.22.1-1 nodever.noarch 0:1.0.0-4
Complete! $ node -v ; npm -v ; nodever list ; node -p 'process.platform + " " + process.arch'
v24.22.1
11.19.1
Running command: alternatives --config node
Consider calling update-alternatives directly next time.
There is only one alternative in link group node (providing /QOpenSys/pkgs/bin/node): /QOpenSys/pkgs/lib/nodejs24/bin/node
Nothing to configure.
os400 ppc64 34 Mo téléchargés, 209 Mo installés dans /QOpenSys/pkgs/lib/nodejs24. Rien à faire
côté IBM i : pas de PTF, pas d'IPL. Le profil qui installe doit avoir le droit spécial *ALLOBJ :
yum écrit dans /QOpenSys/pkgs, qui appartient à QSYS.
Installer depuis une session 5250 (QSH), sans SSH
Sur un IBM i 7.4 de production, le serveur SSH n'était pas démarré : l'installation s'est faite
depuis un simple écran 5250, dans QSH (STRQSH). Deux surprises en chemin, toutes deux
sans gravité une fois comprises.
1. « yum » introuvable. QSH ne cherche pas dans /QOpenSys/pkgs/bin,
où yum installe ses commandes. Le message ne prouve donc pas que l'open source est absent : on
vérifie la présence des fichiers, puis on complète le PATH.
> yum
qsh: 001-0019 Error found searching for command yum. No such path or directory.
> ls -l /QOpenSys/pkgs/bin/yum /QOpenSys/pkgs/bin/rpm
-rwxr-xr-x 1 QSYS 0 149699 Dec 3 2024 /QOpenSys/pkgs/bin/rpm
-rwxr-xr-x 1 QSYS 0 822 Jul 20 2022 /QOpenSys/pkgs/bin/yum
> export PATH=/QOpenSys/pkgs/bin:$PATH
Si ces deux fichiers n'existent pas, l'open source n'a jamais été installé : il faut d'abord
l'amorcer (bootstrap) avec ACS, Outils, Gestion des paquets open source, ce qui demande le serveur
SSH (STRTCPSVR SERVER(*SSHD)).
2. « getaddrinfo() thread failed to start ». La liste des versions s'affiche (elle vient du cache de yum), mais avec une erreur par dépôt, et l'installation échoue faute de pouvoir télécharger quoi que ce soit :
> yum list available 'nodejs*'
https://public.dhe.ibm.com/software/ibmi/products/pase/rpms/repo-base-7.3/repodata/repomd.xml:
[Errno 14] curl#6 - "getaddrinfo() thread failed to start"
Essai d'un autre miroir.
> yum -y install nodejs24 nodever
Erreur durant le téléchargement des paquets :
libuuid1-2.40.4-1.ppc64: failure: ... from ibmi-base: [Errno 256] No more mirrors to try.
python39-3.9.21-2.ppc64: failure: ... from ibmi-base: [Errno 256] No more mirrors to try.
nodejs24-24.22.1-1.ppc64: failure: ... from ibmi-base: [Errno 256] No more mirrors to try.
nodever-1.0.0-4.noarch: failure: ... from ibmi-base: [Errno 256] No more mirrors to try.
Ce n'est pas le réseau : curl résout le nom du serveur d'IBM dans un thread, et un travail QSH n'accepte pas plusieurs threads par défaut. On quitte QSH par F3 (pas F12, qui laisse la session ouverte), on autorise les threads, et on relance :
ADDENVVAR ENVVAR(QIBM_MULTI_THREADED) VALUE(Y) REPLACE(*YES)
STRQSH
> export PATH=/QOpenSys/pkgs/bin:$PATH
> yum -y install nodejs24 nodever
ibmi-base | 3.6 kB 00:00
ibmi-base/primary_db 33% [=====- ] 18 kB/s | 160 kB 00:18 ETA
Le -y est utile : QSH gère mal la question « Is this ok [y/N] ». yum tire au passage
ses dépendances (python39, libuuid1). Le débit peut être faible derrière
le pare-feu d'une entreprise : ne pas interrompre yum (ni F3 ni Appel système) avant le message
final. Autre solution : passer par CALL QP2TERM ou par SSH, qui n'ont pas cette
limite.
Le dépôt repo-base-7.3 qui apparaît dans les messages n'est pas un reste d'une ancienne
version : c'est le dépôt de base d'IBM pour la 7.3 et les versions suivantes. Ses paquets
s'appellent *.ibmi7.3.ppc64.rpm, y compris sur un système en 7.4 ou 7.5.
Un profil de service, pas son propre profil
L'application ne doit pas tourner sous un profil *SECOFR. On crée un profil sans mot de passe ni droit spécial, qui recevra juste ce qu'il faut :
CRTUSRPRF USRPRF(NODEAPP) PASSWORD(*NONE) USRCLS(*USER) SPCAUT(*NONE)
INLMNU(*SIGNOFF) LMTCPB(*YES) HOMEDIR('/www/nodews')
TEXT('Service Node.js webservices NODEWS')
GRTOBJAUT OBJ(QSYS/NODEWS) OBJTYPE(*LIB) USER(NODEAPP) AUT(*USE)
GRTOBJAUT OBJ(NODEWS/ARTICLE) OBJTYPE(*FILE) USER(NODEAPP) AUT(*CHANGE)
GRTOBJAUT OBJ(NODEWS/MOUVEMENT) OBJTYPE(*FILE) USER(NODEAPP) AUT(*CHANGE)
GRTOBJAUT OBJ(NODEWS/CALCDEVIS) OBJTYPE(*PGM) USER(NODEAPP) AUT(*USE)
GRTOBJAUT OBJ(NODEWS/MVTSTOCK) OBJTYPE(*PGM) USER(NODEAPP) AUT(*USE)
Le projet et ses paquets
Le code est copié dans /www/nodews, puis npm ci --omit=dev installe
exactement les versions du package-lock.json. Les pilotes IBM i sont en dépendances
facultatives : le même projet s'installe aussi sur un PC pour les tests unitaires.
{
"name": "nodews-ibmi",
"version": "1.0.0",
"description": "Trois webservices REST Node.js sur IBM i : CRUD Db2 for i, appel d'un programme RPG, transaction SQL",
"private": true,
"type": "module",
"main": "src/server.js",
"engines": {
"node": ">=24"
},
"scripts": {
"start": "node --env-file=.env src/server.js",
"test": "node --test \"test/unit/**/*.test.js\"",
"test:integ": "node --env-file=.env --test --test-concurrency=1 \"test/integration/**/*.test.js\"",
"test:e2e": "node --test \"test/e2e/**/*.test.js\""
},
"dependencies": {
"@fastify/helmet": "^13.2.0",
"@fastify/static": "^10.1.8",
"@fastify/swagger": "^9.9.2",
"@fastify/swagger-ui": "^6.1.2",
"fast-xml-parser": "^5.7.0",
"fastify": "^5.12.7",
"itoolkit": "^1.1.0"
},
"optionalDependencies": {
"idb-connector": "^1.3.1",
"idb-pconnector": "^1.2.0"
},
"devDependencies": {
"playwright-core": "^1.64.1"
},
"license": "MIT"
} Le lancer comme un service
Service Commander (paquet service-commander, déjà là si Mapepire est installé) soumet
Node.js en batch, sous NODEAPP, et surveille le port :
# Service Commander : sc start nodews | sc stop nodews | sc check nodews
# Copié dans /QOpenSys/etc/sc/services/ (déclaration pour tout le système).
# Le travail est soumis en batch sous le profil de service NODEAPP
# (ni *ALLOBJ, ni mot de passe), dans la file non limitée QUSRNOMAX.
name: Webservices Node.js NODEWS
dir: /www/nodews
start_cmd: node --env-file=/www/nodews/.env src/server.js
check_alive: '3030'
batch_mode: 'true'
sbmjob_jobname: NODEWS
sbmjob_opts: 'JOBQ(QUSRNOMAX) USER(NODEAPP)'
environment_vars:
- PATH=/QOpenSys/pkgs/bin:/QOpenSys/usr/bin:/usr/bin
- NODE_ENV=production $ cp /www/nodews/service/nodews.yaml /QOpenSys/etc/sc/services/
$ sc start nodews
Performing operation 'START' on service 'nodews'
Service 'Webservices Node.js NODEWS' successfully started
$ sc check nodews
RUNNING | nodews (Webservices Node.js NODEWS)
JOB_NAME | AUTHORIZATION_NAME | JOB_STATUS | SUBSYSTEM
----------------------+--------------------+------------+----------
151924/NODEAPP/NODEWS | NODEAPP | SELW | QUSRWRK
La clé API est rangée dans un fichier .env lisible par NODEAPP seul (droits 600), lu
par node --env-file : aucun secret dans le code.
Mettre du HTTPS devant Node.js
En HTTP, la clé API circulerait en clair. Plutôt que de gérer un certificat dans Node.js, on place
devant lui une instance de l'IBM HTTP Server (Apache) en reverse proxy :
Apache chiffre sur le port 3443 avec un certificat géré par DCM, et relaie en local à Node.js, qui
n'écoute plus que sur 127.0.0.1 (HOST=127.0.0.1 dans le
.env). Le port 3030 n'est plus joignable de l'extérieur.
# NODEWS - Instance IBM HTTP Server (Apache) en reverse proxy HTTPS devant Node.js
# Apache termine le TLS sur *:3443 et relaie vers Node.js, qui n'écoute plus qu'en
# local sur 127.0.0.1:3030 (HOST=127.0.0.1 dans /www/nodews/.env).
# Certificat : ID d'application DCM QIBM_HTTP_SERVER_FORMATIONS (partagé avec
# FORMATIONS et WSCLI), CN=nom DNS de la machine, émis par l'autorité interne Armonie.
LoadModule ibm_ssl_module /QSYS.LIB/QHTTPSVR.LIB/QZSRVSSL.SRVPGM
LoadModule proxy_module /QSYS.LIB/QHTTPSVR.LIB/QZSRCORE.SRVPGM
LoadModule proxy_http_module /QSYS.LIB/QHTTPSVR.LIB/QZSRCORE.SRVPGM
DefaultNetCCSID 1208
Listen *:3443
ServerName mon-ibmi.exemple.fr
TraceEnable Off
ServerSignature Off
Options -FollowSymLinks
LogFormat "%h %T %l %u %t \"%r\" %>s %b" combined
CustomLog logs/access_log combined
ErrorLog logs/error_log
LogMaint logs/access_log 7 0
LogMaint logs/error_log 7 0
LogLevel warn
<Directory />
Require all denied
</Directory>
# Reverse proxy uniquement (jamais de proxy ouvert)
ProxyRequests Off
ProxyPreserveHost On
ProxyTimeout 60
<VirtualHost *:3443>
SSLEngine On
SSLAppName QIBM_HTTP_SERVER_FORMATIONS
SSLProtocolDisable SSLv3 TLSv1 TLSv1.1
ProxyPass / http://127.0.0.1:3030/
ProxyPassReverse / http://127.0.0.1:3030/
</VirtualHost>
HTTPSubsystemDesc QHTTPSVR/QHTTPSVR
HTTPStartJobDesc QHTTPSVR/QZHBHTTP
HTTPStartJobQueue *JOBD
HTTPRoutingData *JOBD L'instance se déclare comme toutes les instances HTTP, par un membre de QUSRSYS/QATMHINSTC (un fichier à une seule colonne, rempli par SQL au travers d'un alias), puis se démarre :
ADDPFM FILE(QUSRSYS/QATMHINSTC) MBR(NODEWS) TEXT('Reverse proxy HTTPS Node.js NODEWS')
CREATE OR REPLACE ALIAS NODEWS.A_INST FOR QUSRSYS.QATMHINSTC(NODEWS);
INSERT INTO NODEWS.A_INST VALUES('-apache -d /www/nodews/apache -f conf/httpd.conf');
DROP ALIAS NODEWS.A_INST;
STRTCPSVR SERVER(*HTTP) HTTPSVR(NODEWS)
TCP1A0F HTTP server starting.
LOCAL_ADDRESS | LOCAL_PORT | TCP_STATE
--------------+------------+----------
0.0.0.0 | 3443 | LISTEN
127.0.0.1 | 3030 | LISTEN
$ openssl s_client -connect mon-ibmi.exemple.fr:3443
Protocol: TLSv1.3 Cipher is TLS_AES_128_GCM_SHA256
(TLS 1.0 et TLS 1.1 refusés)
Le répertoire logs de l'instance doit appartenir à QTMHHTTP, le profil des travaux
Apache. Le certificat venant d'une autorité interne, les clients doivent la connaître : Chrome et
Postman la lisent dans le magasin Windows ; newman, qui tourne sous Node.js, a besoin de
NODE_OPTIONS=--use-system-ca.
Étape 2 La base
Les tables Db2 for i
Un schéma SQL (donc journalisé, ce qu'exige l'exemple 3), une table d'articles et une table de mouvements, avec des contraintes : c'est Db2 qui garantit un code unique, un prix positif, un taux de TVA connu et un stock jamais négatif.
-- =====================================================================
-- NODEWS : schéma de démonstration des webservices Node.js sur IBM i
-- Cible : un IBM i 7.5, à exécuter une fois (ACS Run SQL Scripts
-- ou ibmi.py sql). CREATE SCHEMA crée aussi le journal QSQJRN : toutes
-- les tables sont journalisées, donc utilisables sous contrôle de
-- validation (indispensable pour l'exemple 3 transactionnel).
-- =====================================================================
CREATE SCHEMA NODEWS;
-- LABEL ON SCHEMA n'existe pas en Db2 for i (SQL0104) : le texte de la
-- bibliothèque se pose par CHGOBJD, appelé depuis SQL.
CALL QSYS2.QCMDEXC('CHGOBJD OBJ(QSYS/NODEWS) OBJTYPE(*LIB) TEXT(''Demo webservices Node.js'')');
-- Les noms de contraintes non qualifiés prennent le schéma par défaut du
-- job (le profil, ici SAK) : sans SET SCHEMA, SQL5051 sur ARTICLE_PK.
SET SCHEMA NODEWS;
-- ---------------------------------------------------------------------
-- Catalogue d'articles (exemple 1 : CRUD REST)
-- Noms SQL longs pour Node.js, noms système courts pour le 5250 (DSPFFD,
-- RPG en colonnes, Query/400).
-- ---------------------------------------------------------------------
-- Pas de FOR SYSTEM NAME : ARTICLE est déjà un nom système valide (SQL7029)
CREATE TABLE NODEWS.ARTICLE (
ID INTEGER GENERATED ALWAYS AS IDENTITY
(START WITH 1 INCREMENT BY 1),
CODE VARCHAR(15) NOT NULL,
LIBELLE VARCHAR(80) NOT NULL,
FAMILLE VARCHAR(30) NOT NULL DEFAULT 'DIVERS',
PRIX_HT FOR COLUMN PRIXHT DECIMAL(11, 2) NOT NULL DEFAULT 0,
TAUX_TVA FOR COLUMN TAUXTVA DECIMAL(5, 2) NOT NULL DEFAULT 20,
STOCK INTEGER NOT NULL DEFAULT 0,
DATE_CREATION FOR COLUMN DATCRE TIMESTAMP NOT NULL
DEFAULT CURRENT TIMESTAMP,
DATE_MAJ FOR COLUMN DATMAJ TIMESTAMP NOT NULL
GENERATED ALWAYS FOR EACH ROW ON UPDATE
AS ROW CHANGE TIMESTAMP,
CONSTRAINT ARTICLE_PK PRIMARY KEY (ID),
CONSTRAINT ARTICLE_CODE_UK UNIQUE (CODE),
CONSTRAINT ARTICLE_PRIX_CK CHECK (PRIX_HT >= 0),
CONSTRAINT ARTICLE_TVA_CK CHECK (TAUX_TVA IN (0, 2.1, 5.5, 10, 20)),
CONSTRAINT ARTICLE_STK_CK CHECK (STOCK >= 0)
) RCDFMT ARTICLER;
LABEL ON TABLE NODEWS.ARTICLE IS 'Catalogue articles (demo Node.js)';
LABEL ON COLUMN NODEWS.ARTICLE (
ID TEXT IS 'Identifiant',
CODE TEXT IS 'Code article',
LIBELLE TEXT IS 'Libelle',
FAMILLE TEXT IS 'Famille',
PRIX_HT TEXT IS 'Prix unitaire HT',
TAUX_TVA TEXT IS 'Taux de TVA',
STOCK TEXT IS 'Quantite en stock',
DATE_CREATION TEXT IS 'Date de creation',
DATE_MAJ TEXT IS 'Date de derniere modification'
);
-- Index pour la recherche par famille (filtre ?famille= de l'API)
CREATE INDEX NODEWS.ARTICLE_FAMILLE_IX FOR SYSTEM NAME ARTICLE1
ON NODEWS.ARTICLE (FAMILLE, CODE);
-- ---------------------------------------------------------------------
-- Mouvements de stock (exemple 3 : transaction)
-- ---------------------------------------------------------------------
CREATE TABLE NODEWS.MOUVEMENT (
ID INTEGER GENERATED ALWAYS AS IDENTITY,
ARTICLE_ID FOR COLUMN ARTID INTEGER NOT NULL,
TYPE_MVT FOR COLUMN TYPMVT CHAR(1) NOT NULL,
QUANTITE FOR COLUMN QTE INTEGER NOT NULL,
STOCK_APRES FOR COLUMN STKAPR INTEGER NOT NULL,
COMMENTAIRE FOR COLUMN COMMEN VARCHAR(100) NOT NULL DEFAULT '',
UTILISATEUR FOR COLUMN UTIL VARCHAR(18) NOT NULL DEFAULT USER,
DATE_MVT FOR COLUMN DATMVT TIMESTAMP NOT NULL
DEFAULT CURRENT TIMESTAMP,
CONSTRAINT MOUVEMENT_PK PRIMARY KEY (ID),
CONSTRAINT MOUVEMENT_ART_FK FOREIGN KEY (ARTICLE_ID)
REFERENCES NODEWS.ARTICLE (ID)
ON DELETE RESTRICT,
CONSTRAINT MOUVEMENT_TYPE_CK CHECK (TYPE_MVT IN ('E', 'S')),
CONSTRAINT MOUVEMENT_QTE_CK CHECK (QUANTITE > 0)
) RCDFMT MOUVEMENTR;
LABEL ON TABLE NODEWS.MOUVEMENT IS 'Mouvements de stock (demo Node.js)';
CREATE INDEX NODEWS.MOUVEMENT_ARTICLE_IX FOR SYSTEM NAME MOUVEMENT1
ON NODEWS.MOUVEMENT (ARTICLE_ID, DATE_MVT DESC); Exemple 1 CRUD
Un CRUD REST sur une table Db2
Cinq routes : GET /api/articles (liste paginée, filtres), GET,
POST, PUT et DELETE /api/articles/{id}. L'accès à Db2 tient
dans un petit module autour du pool de connexions d'idb-pconnector :
// Accès Db2 for i par idb-pconnector : pilote natif PASE, la connexion
// s'ouvre dans le travail du serveur Node.js (*LOCAL), sans mot de passe,
// avec les droits du profil qui exécute le travail.
const SQLSTATE = /SQLSTATE=(\w{5})/;
/** Extrait le SQLSTATE et le texte Db2 d'une erreur idb (enfouis dans la pile). */
export function sqlInfo(error) {
const texte = `${error?.message ?? ''}\n${error?.stack ?? ''}`;
const etat = texte.match(SQLSTATE)?.[1] ?? null;
const ligne = texte.split('\n').find((l) => SQLSTATE.test(l)) ?? '';
return { sqlState: etat, detail: ligne.trim() };
}
export async function createDb({ poolSize = 4 } = {}) {
// Import dynamique : le module n'existe que sur IBM i, les tests unitaires
// du poste n'en ont pas besoin.
const { default: idb } = await import('idb-pconnector');
const pool = new idb.DBPool(
{ url: '*LOCAL' },
// Sans enableNumericTypeConversion, les DECIMAL arrivent en chaînes
{ incrementSize: poolSize, enableNumericTypeConversion: true },
);
const executer = async (sql, params, options) => {
try {
return await pool.prepareExecute(sql, params, options);
} catch (error) {
Object.assign(error, sqlInfo(error));
throw error;
}
};
return {
/** SELECT : renvoie toujours un tableau de lignes. */
async query(sql, params = []) {
const r = await executer(sql, params, { io: 'in' });
return r?.resultSet ?? [];
},
/** INSERT, UPDATE, DELETE sans résultat. */
async execute(sql, params = []) {
await executer(sql, params, { io: 'in' });
},
/** CALL avec paramètres OUT : renvoie les valeurs de sortie dans l'ordre. */
async call(sql, params = []) {
const r = await executer(sql, params, {});
return r?.outputParams ?? [];
},
async close() {
await pool.retireAll();
},
};
} Le dépôt d'articles n'écrit que des requêtes à marqueurs ? : aucune saisie n'est
collée dans le SQL. FINAL TABLE rend l'identifiant créé sans second aller-retour.
// Exemple 1 : accès à NODEWS.ARTICLE. Toujours des marqueurs « ? » :
// jamais de valeur saisie concaténée dans le SQL (injection impossible).
const COLONNES = `
ID AS "id",
CODE AS "code",
LIBELLE AS "libelle",
FAMILLE AS "famille",
PRIX_HT AS "prixHt",
TAUX_TVA AS "tauxTva",
STOCK AS "stock",
VARCHAR_FORMAT(DATE_CREATION, 'YYYY-MM-DD HH24:MI:SS') AS "dateCreation",
VARCHAR_FORMAT(DATE_MAJ, 'YYYY-MM-DD HH24:MI:SS') AS "dateMaj"`;
export function createArticleRepository(db, schema) {
const table = `${schema}.ARTICLE`;
return {
async list({ famille, q, limit = 50, offset = 0 } = {}) {
const where = [];
const params = [];
if (famille) {
where.push('FAMILLE = ?');
params.push(famille.toUpperCase());
}
if (q) {
where.push('(UPPER(LIBELLE) LIKE ? OR CODE LIKE ?)');
const motif = `%${q.toUpperCase()}%`;
params.push(motif, motif);
}
const clause = where.length ? `WHERE ${where.join(' AND ')}` : '';
const [{ total }] = await db.query(
`SELECT COUNT(*) AS "total" FROM ${table} ${clause}`, params);
const items = await db.query(
`SELECT ${COLONNES} FROM ${table} ${clause}
ORDER BY CODE
OFFSET ? ROWS FETCH FIRST ? ROWS ONLY`,
[...params, offset, limit]);
return { total, limit, offset, items };
},
async get(id) {
const [ligne] = await db.query(
`SELECT ${COLONNES} FROM ${table} WHERE ID = ?`, [id]);
return ligne ?? null;
},
async create(a) {
// FINAL TABLE renvoie l'identité générée sans second aller-retour
const [{ id }] = await db.query(
`SELECT ID AS "id" FROM FINAL TABLE (
INSERT INTO ${table} (CODE, LIBELLE, FAMILLE, PRIX_HT, TAUX_TVA, STOCK)
VALUES (?, ?, ?, ?, ?, ?))`,
[a.code.toUpperCase(), a.libelle, (a.famille ?? 'DIVERS').toUpperCase(),
a.prixHt, a.tauxTva ?? 20, a.stock ?? 0]);
return this.get(id);
},
async update(id, a) {
if (!(await this.get(id))) return null;
await db.execute(
`UPDATE ${table}
SET CODE = ?, LIBELLE = ?, FAMILLE = ?, PRIX_HT = ?, TAUX_TVA = ?
WHERE ID = ?`,
[a.code.toUpperCase(), a.libelle, (a.famille ?? 'DIVERS').toUpperCase(),
a.prixHt, a.tauxTva ?? 20, id]);
return this.get(id);
},
async remove(id) {
if (!(await this.get(id))) return false;
await db.execute(`DELETE FROM ${table} WHERE ID = ?`, [id]);
return true;
},
async familles() {
return db.query(
`SELECT FAMILLE AS "famille", COUNT(*) AS "articles",
SUM(STOCK) AS "stock", SUM(STOCK * PRIX_HT) AS "valeurHt"
FROM ${table} GROUP BY FAMILLE ORDER BY FAMILLE`);
},
};
} 

Exemple 2 Programme RPG
Un programme RPG existant devient un webservice
CALCDEVIS est un programme RPG full free ordinaire : un code article et une quantité en entrée, le devis en sortie. Il ne sait rien de HTTP ni de JSON, et n'a pas à être modifié.
**free
// ====================================================================
// CALCDEVIS : calcul d'une ligne de devis (exemple 2 Node.js)
// Programme RPG "historique" appelé tel quel par Node.js via
// XMLSERVICE (itoolkit) : aucune modification n'est nécessaire pour
// l'exposer en webservice.
//
// Entrée : code article (15A), quantité (7P0)
// Sortie : libellé, PU HT, % remise, total HT, TVA, TTC,
// code retour (OK / INC / QTE / STK) et message
// Remise : >= 100 -> 10 %, >= 50 -> 5 %, >= 10 -> 2 %
// ====================================================================
ctl-opt dftactgrp(*no) actgrp(*caller) option(*srcstmt: *nodebugio)
text('Calcul de devis - demo Node.js');
dcl-pi *n;
pCode char(15) const;
pQte packed(7: 0) const;
pLibelle char(80);
pPuHt packed(11: 2);
pRemise packed(5: 2);
pTotalHt packed(13: 2);
pTva packed(13: 2);
pTtc packed(13: 2);
pRetour char(3);
pMessage char(80);
end-pi;
dcl-s wLibelle varchar(80);
dcl-s wPrix packed(11: 2);
dcl-s wTaux packed(5: 2);
dcl-s wStock int(10);
dcl-s wBrut packed(15: 2);
exec sql set option commit = *none, closqlcsr = *endmod;
clear pLibelle;
clear pPuHt;
clear pRemise;
clear pTotalHt;
clear pTva;
clear pTtc;
pRetour = 'OK';
pMessage = 'Devis calcule';
if pQte <= 0;
pRetour = 'QTE';
pMessage = 'La quantite doit etre superieure a zero';
return;
endif;
exec sql
select LIBELLE, PRIX_HT, TAUX_TVA, STOCK
into :wLibelle, :wPrix, :wTaux, :wStock
from NODEWS.ARTICLE
where CODE = upper(trim(:pCode));
if sqlcode = 100;
pRetour = 'INC';
pMessage = 'Article ' + %trim(pCode) + ' inconnu';
return;
elseif sqlcode < 0;
pRetour = 'ERR';
pMessage = 'Erreur SQL ' + %char(sqlcode);
return;
endif;
pLibelle = wLibelle;
pPuHt = wPrix;
select;
when pQte >= 100;
pRemise = 10;
when pQte >= 50;
pRemise = 5;
when pQte >= 10;
pRemise = 2;
other;
pRemise = 0;
endsl;
wBrut = wPrix * pQte;
pTotalHt = %dech(wBrut * (100 - pRemise) / 100: 13: 2);
pTva = %dech(pTotalHt * wTaux / 100: 13: 2);
pTtc = pTotalHt + pTva;
// Stock insuffisant : le devis est calculé quand même, avec un avertissement
if pQte > wStock;
pRetour = 'STK';
pMessage = 'Stock insuffisant : ' + %char(wStock) + ' disponible(s)';
endif;
return; Côté Node.js, itoolkit décrit les dix paramètres dans l'ordre du PI et appelle le programme par XMLSERVICE, en local :
// Exemple 2 : appel du programme RPG NODEWS/CALCDEVIS par XMLSERVICE
// (itoolkit, transport idb : appel en local, dans le travail Node.js).
import itoolkit from 'itoolkit';
import { XMLParser } from 'fast-xml-parser';
const { Connection, ProgramCall } = itoolkit;
// itoolkit recopie les valeurs telles quelles dans le XML : on échappe
// nous-mêmes, sinon un « < » dans une saisie casserait (ou détournerait)
// la requête XMLSERVICE.
export function xmlEscape(valeur) {
return String(valeur)
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll("'", ''')
.replaceAll('"', '"');
}
// Interface du programme, dans l'ordre des paramètres du PI RPG
export const PARAMETRES = [
{ nom: 'code', type: '15A' },
{ nom: 'quantite', type: '7p0' },
{ nom: 'libelle', type: '80A' },
{ nom: 'prixUnitaireHt', type: '11p2' },
{ nom: 'remisePct', type: '5p2' },
{ nom: 'totalHt', type: '13p2' },
{ nom: 'tva', type: '13p2' },
{ nom: 'totalTtc', type: '13p2' },
{ nom: 'retour', type: '3A' },
{ nom: 'message', type: '80A' },
];
const parser = new XMLParser({ ignoreAttributes: false, parseTagValue: false });
/** Transforme la réponse XMLSERVICE en objet ; lève une erreur si l'appel a échoué. */
export function lireReponse(xml) {
const doc = parser.parse(xml);
const pgm = doc?.myscript?.pgm;
if (!pgm || pgm.error || !pgm.success) {
const detail = pgm?.error?.errnoxml ?? pgm?.error ?? 'réponse XMLSERVICE illisible';
throw new Error(`Appel CALCDEVIS en échec : ${JSON.stringify(detail)}`);
}
const parms = [pgm.parm].flat();
const valeurs = {};
PARAMETRES.forEach((p, i) => {
const brut = parms[i]?.data?.['#text'] ?? '';
// XMLSERVICE formate les décimaux selon le travail : « 1450,00 » sur un
// IBM i en français. Number('1450,00') donnerait NaN.
valeurs[p.nom] = p.type.endsWith('A') ? brut.trim() : Number(brut.replace(',', '.'));
});
return valeurs;
}
export function createDevisService({ lib }) {
return {
async calculer(code, quantite) {
const conn = new Connection({
transport: 'idb',
transportOptions: { database: '*LOCAL' },
});
const pgm = new ProgramCall('CALCDEVIS', { lib, error: 'off' });
const entree = { code: xmlEscape(code.toUpperCase()), quantite: String(quantite) };
for (const p of PARAMETRES) {
pgm.addParam({ type: p.type, value: entree[p.nom] ?? (p.type.endsWith('A') ? '' : '0') });
}
conn.add(pgm);
const xml = await new Promise((resolve, reject) => {
conn.run((err, sortie) => (err ? reject(err) : resolve(sortie)));
});
return lireReponse(xml);
},
};
} // Exemple 2 : le programme RPG CALCDEVIS exposé en webservice
import { HttpError } from '../errors.js';
import { devis, devisSaisie, erreur } from '../schemas.js';
// Codes retour du programme RPG -> statut HTTP
const STATUT = { OK: 200, STK: 200, INC: 404, QTE: 400 };
export default async function devisRoutes(app, { devisService }) {
app.post('/devis', {
schema: {
tags: ['Exemple 2 - Programme RPG'],
summary: 'Calcul de devis par le programme RPG NODEWS/CALCDEVIS',
body: devisSaisie,
response: { 200: devis, 400: erreur, 404: erreur },
},
}, async (request) => {
const { code, quantite } = request.body;
const r = await devisService.calculer(code, quantite);
const statut = STATUT[r.retour] ?? 500;
if (statut !== 200) throw new HttpError(statut, r.message);
return { code: code.toUpperCase(), quantite, ...r };
});
} 
Exemple 3 Transaction
Un mouvement de stock, tout ou rien
Une sortie de stock, c'est deux écritures : le stock et l'historique. Elles doivent réussir ensemble ou pas du tout. La règle est dans une procédure SQL, sous contrôle de validation ; en cas d'erreur, elle annule tout et renvoie un SQLSTATE que Node.js traduit en statut HTTP.
-- =====================================================================
-- NODEWS.MVT_STOCK : entrée ou sortie de stock en une seule transaction
-- (exemple 3). La règle métier reste dans Db2 : Node.js se contente
-- d'appeler la procédure et de traduire les SQLSTATE en codes HTTP.
-- 75001 -> article inconnu (HTTP 404)
-- 75002 -> stock insuffisant (HTTP 409)
-- 75003 -> type ou quantité faux (HTTP 400)
-- =====================================================================
CREATE OR REPLACE PROCEDURE NODEWS.MVT_STOCK (
IN P_ARTICLE_ID INTEGER,
IN P_TYPE CHAR(1),
IN P_QUANTITE INTEGER,
IN P_COMMENTAIRE VARCHAR(100),
OUT P_STOCK_APRES INTEGER,
OUT P_MVT_ID INTEGER
)
LANGUAGE SQL
SPECIFIC NODEWS.MVTSTOCK
MODIFIES SQL DATA
COMMIT ON RETURN YES
SET OPTION COMMIT = *CHG, DBGVIEW = *SOURCE, USRPRF = *USER
BEGIN
DECLARE V_NB INTEGER DEFAULT 0;
-- Toute erreur annule l'ensemble (mise à jour du stock + historique)
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
RESIGNAL;
END;
IF P_TYPE NOT IN ('E', 'S') OR P_QUANTITE IS NULL OR P_QUANTITE <= 0 THEN
SIGNAL SQLSTATE '75003'
SET MESSAGE_TEXT = 'Type (E ou S) ou quantite (> 0) invalide';
END IF;
-- Une seule instruction, donc pas de lecture puis écriture : deux
-- sorties simultanées ne peuvent pas rendre le stock négatif.
UPDATE NODEWS.ARTICLE
SET STOCK = CASE P_TYPE WHEN 'E' THEN STOCK + P_QUANTITE
ELSE STOCK - P_QUANTITE END
WHERE ID = P_ARTICLE_ID
AND (P_TYPE = 'E' OR STOCK >= P_QUANTITE);
GET DIAGNOSTICS V_NB = ROW_COUNT;
IF V_NB = 0 THEN
IF NOT EXISTS (SELECT 1 FROM NODEWS.ARTICLE WHERE ID = P_ARTICLE_ID) THEN
SIGNAL SQLSTATE '75001' SET MESSAGE_TEXT = 'Article inconnu';
ELSE
SIGNAL SQLSTATE '75002' SET MESSAGE_TEXT = 'Stock insuffisant';
END IF;
END IF;
SELECT STOCK INTO P_STOCK_APRES FROM NODEWS.ARTICLE WHERE ID = P_ARTICLE_ID;
SELECT ID INTO P_MVT_ID FROM FINAL TABLE (
INSERT INTO NODEWS.MOUVEMENT
(ARTICLE_ID, TYPE_MVT, QUANTITE, STOCK_APRES, COMMENTAIRE)
VALUES (P_ARTICLE_ID, P_TYPE, P_QUANTITE, P_STOCK_APRES,
COALESCE(P_COMMENTAIRE, ''))
);
END;
LABEL ON SPECIFIC PROCEDURE NODEWS.MVTSTOCK IS 'Mouvement de stock transactionnel'; // Exemple 3 : mouvements de stock. La transaction (mise à jour du stock +
// historique, tout ou rien) est faite par la procédure SQL MVT_STOCK.
export function createMouvementRepository(db, schema) {
return {
async enregistrer(articleId, { type, quantite, commentaire = '' }) {
const [stockApres, mouvementId] = await db.call(
`CALL ${schema}.MVT_STOCK(?, ?, ?, ?, ?, ?)`,
[
{ value: articleId, io: 'in' },
{ value: type, io: 'in' },
{ value: quantite, io: 'in' },
{ value: commentaire, io: 'in' },
{ value: 0, io: 'out' },
{ value: 0, io: 'out' },
]);
return { mouvementId, articleId, type, quantite, stockApres };
},
async historique(articleId, limit = 20) {
return db.query(
`SELECT ID AS "id", TYPE_MVT AS "type", QUANTITE AS "quantite",
STOCK_APRES AS "stockApres", COMMENTAIRE AS "commentaire",
UTILISATEUR AS "utilisateur",
VARCHAR_FORMAT(DATE_MVT, 'YYYY-MM-DD HH24:MI:SS') AS "date"
FROM ${schema}.MOUVEMENT
WHERE ARTICLE_ID = ?
ORDER BY DATE_MVT DESC, ID DESC
FETCH FIRST ? ROWS ONLY`,
[articleId, limit]);
},
};
} 
Les bonnes pratiques appliquées
- Schémas JSON sur chaque route : Fastify refuse un corps mal formé (400) avant
tout appel à l'IBM i, et en tire la documentation OpenAPI (
/docs). - Clé API pour toute écriture, comparée à temps constant, et masquée dans les journaux.
- En-têtes de sécurité (Helmet), corps limité à 64 Ko.
- Erreurs homogènes : un SQLSTATE connu devient un statut clair, le reste un 500 sans détail technique ; le détail part dans le journal.
- Injection de dépendances : l'application se teste sans IBM i.
- Arrêt propre sur SIGTERM : les requêtes en cours se terminent, le pool est rendu.
- Profil de service dédié,
.enven 600,npm ciavec fichier de verrouillage.
// Fabrique de l'application : les dépendances (base, programme RPG) sont
// injectées, ce qui permet aux tests unitaires de tourner sans IBM i.
import { timingSafeEqual } from 'node:crypto';
import { fileURLToPath } from 'node:url';
import Fastify from 'fastify';
import helmet from '@fastify/helmet';
import fastifyStatic from '@fastify/static';
import swagger from '@fastify/swagger';
import swaggerUi from '@fastify/swagger-ui';
import { errorHandler } from './errors.js';
import { article } from './schemas.js';
import articlesRoutes from './routes/articles.js';
import devisRoutes from './routes/devis.js';
import mouvementsRoutes from './routes/mouvements.js';
const PUBLIC = fileURLToPath(new URL('../public/', import.meta.url));
function cleValide(recue, attendue) {
const a = Buffer.from(String(recue ?? ''));
const b = Buffer.from(attendue);
// Comparaison à temps constant : pas d'attaque par mesure du temps
return a.length === b.length && timingSafeEqual(a, b);
}
export async function buildApp({ config, deps, logger = true }) {
const app = Fastify({
logger: logger && {
level: config.logLevel,
redact: ['req.headers["x-api-key"]'],
},
bodyLimit: 64 * 1024,
ajv: { customOptions: { coerceTypes: 'array', removeAdditional: false } },
});
app.setErrorHandler(errorHandler);
app.addSchema(article);
await app.register(helmet, {
contentSecurityPolicy: {
directives: {
'default-src': ["'self'"],
'style-src': ["'self'", 'https://fonts.googleapis.com', "'unsafe-inline'"],
'font-src': ["'self'", 'https://fonts.gstatic.com'],
'img-src': ["'self'", 'data:'],
// le site est servi en HTTP sur le réseau interne : pas de upgrade forcé
'upgrade-insecure-requests': null,
},
},
hsts: false,
});
await app.register(swagger, {
openapi: {
info: {
title: 'Webservices Node.js sur IBM i',
description: 'Trois exemples : CRUD Db2 for i, appel d\'un programme RPG, transaction SQL.',
version: '1.0.0',
},
components: {
securitySchemes: { cleApi: { type: 'apiKey', in: 'header', name: 'x-api-key' } },
},
},
});
await app.register(swaggerUi, { routePrefix: '/docs' });
// Clé API exigée pour toute route déclarée en écriture
app.addHook('onRequest', async (request, reply) => {
if (request.routeOptions.config?.ecriture
&& !cleValide(request.headers['x-api-key'], config.apiKey)) {
reply.code(401).send({
statusCode: 401, erreur: 'Unauthorized', message: 'Clé API absente ou invalide (en-tête x-api-key)',
});
}
});
await app.register(async (api) => {
api.get('/sante', {
schema: { tags: ['Supervision'], summary: 'État du service et de la base' },
}, async () => ({
statut: 'ok',
node: process.version,
plateforme: `${process.platform} ${process.arch}`,
base: await deps.sante(),
dureeMarche: Math.round(process.uptime()),
}));
await api.register(articlesRoutes, deps);
await api.register(devisRoutes, deps);
await api.register(mouvementsRoutes, deps);
}, { prefix: '/api' });
// Écran de test (design IBM Carbon)
await app.register(fastifyStatic, { root: PUBLIC, prefix: '/' });
return app;
} 
Les tests : unitaires, IBM i, Chrome, Postman
Tests unitaires, avec le lanceur intégré à Node.js (node:test) : des
doublures en mémoire remplacent Db2 et le RPG, ils tournent sur un PC en moins d'une seconde.
# tests 39
# pass 39
# fail 0
# duration_ms 799.2135 Tests d'intégration, lancés sur l'IBM i : vraie table, vrai RPG, vraie procédure, accents compris.
✔ santé : connexion *LOCAL (32.276291ms)
✔ Exemple 1 : création, lecture, doublon, modification (60.607349ms)
✔ Exemple 1 : contrainte CHECK Db2 -> SQLSTATE 23513 (14.177099ms)
✔ Exemple 2 : programme RPG CALCDEVIS via XMLSERVICE (84.255697ms)
✔ Exemple 2 : article inconnu -> 404 renvoyé par le RPG (76.680668ms)
✔ Exemple 3 : entrée puis sortie, historique (47.087985ms)
✔ Exemple 3 : stock insuffisant -> 409 et rien n'est écrit (ROLLBACK) (10.858983ms)
✔ Exemple 3 : article inconnu -> 404 (SIGNAL 75001) (3.573526ms)
✔ Exemple 1 : suppression refusée tant qu'il y a des mouvements (clé étrangère) (6.401603ms)
✔ requêtes concurrentes : 20 lectures en parallèle sur le pool (141.883452ms)
✔ Db2 for i réel (891.617043ms)
ℹ tests 10
ℹ pass 10
ℹ fail 0 Tests dans Chrome : Playwright pilote l'écran de test (création sans clé, création, doublon, modification, recherche, devis, mouvements, suppression refusée), 11 sur 11.

Postman : une collection de 17 requêtes et 58 assertions, créée dans Postman puis exécutée par newman depuis le poste.

√ 409 tant qu'il existe des mouvements
┌─────────────────────────┬───────────────────┬──────────────────┐
│ │ executed │ failed │
├─────────────────────────┼───────────────────┼──────────────────┤
│ iterations │ 1 │ 0 │
├─────────────────────────┼───────────────────┼──────────────────┤
│ requests │ 17 │ 0 │
├─────────────────────────┼───────────────────┼──────────────────┤
│ test-scripts │ 34 │ 0 │
├─────────────────────────┼───────────────────┼──────────────────┤
│ prerequest-scripts │ 1 │ 0 │
├─────────────────────────┼───────────────────┼──────────────────┤
│ assertions │ 58 │ 0 │
├─────────────────────────┴───────────────────┴──────────────────┤
│ total run duration: 1985ms │
├────────────────────────────────────────────────────────────────┤
│ total data received: 4.46kB (approx) │
├────────────────────────────────────────────────────────────────┤
│ average response time: 34ms [min: 11ms, max: 88ms, s.d.: 27ms] │
└────────────────────────────────────────────────────────────────┘ 
Les erreurs rencontrées
Les deux premières sont apparues lors d'une installation depuis QSH, sur un IBM i 7.4 de production ; les autres, dans l'ordre où elles sont apparues pendant la mise en place sur l'IBM i de formation. Deux d'entre elles (les DECIMAL en chaînes, l'injection XML) ont été repérées en lisant le code des paquets, avant d'écrire l'application : elles n'ont donc pas eu le temps de se produire.
| Message ou symptôme | Cause | Correctif |
|---|---|---|
qsh: 001-0019 Error found searching for command yum | QSH ne cherche pas dans /QOpenSys/pkgs/bin, où yum installe ses commandes. | Vérifier par ls -l /QOpenSys/pkgs/bin/yum, puis export PATH=/QOpenSys/pkgs/bin:$PATH. |
yum : curl#6 - "getaddrinfo() thread failed to start", puis [Errno 256] No more mirrors to try | Le travail QSH n'accepte pas plusieurs threads ; curl en a besoin pour résoudre le nom du dépôt. | Quitter QSH par F3, ADDENVVAR ENVVAR(QIBM_MULTI_THREADED) VALUE(Y), puis STRQSH. Ou passer par QP2TERM ou SSH. |
bsh: PATH=…: is not an identifier | Le shell par défaut du profil est bsh, un Bourne ancien qui refuse « export VAR=valeur ». | Lancer les commandes par /QOpenSys/pkgs/bin/bash -c, ou passer le profil en bash (chsh). |
SQL0104 sur LABEL ON SCHEMA | LABEL ON SCHEMA n'existe pas en Db2 for i. | Texte de la bibliothèque par CHGOBJD, appelé en SQL par CALL QSYS2.QCMDEXC. |
SQL7029 : le nom système ARTICLE ne peut pas être spécifié | FOR SYSTEM NAME donné alors que le nom SQL est déjà un nom système valide. | Ne mettre FOR SYSTEM NAME que si le nom SQL dépasse 10 caractères. |
SQL5051 : qualificatif SAK différent du nom NODEWS | Les noms de contraintes non qualifiés prennent le schéma par défaut du travail, ici le profil. | SET SCHEMA NODEWS avant les CREATE TABLE. |
Cannot find module …\test\unit | node --test ne prend pas un répertoire en argument. | Un motif : node --test "test/unit/**/*.test.js". |
npm warn install-scripts … idb-connector (install: node-gyp-build) | npm 11.19 n'exécute plus les scripts d'installation des paquets non approuvés. | Ici sans conséquence : le binaire IBM i est livré dans prebuilds/. Le vérifier par un require et une requête. |
Réponse 500, totaux à NaN | XMLSERVICE renvoie les décimaux au format du travail : « 1450,00 » sur un système en français. | Remplacer la virgule par un point avant Number(). Test unitaire ajouté. |
Prix en chaînes de caractères dans le JSON | idb-pconnector rend les DECIMAL en chaînes par défaut. | enableNumericTypeConversion: true à la création du DBPool. |
Injection XML possible | itoolkit recopie les valeurs telles quelles dans le XML envoyé à XMLSERVICE. | Échapper & < > ' " soi-même, et valider le code article par un schéma (lettres, chiffres, tiret). |
Postman : « Sending request… » sans fin | L'agent cloud de Postman part de serveurs hors de France ; le filtrage en amont de la machine ne laisse passer que des adresses françaises. | Exécuter la collection depuis le poste, par newman (ou le Postman CLI connecté). |
curl : CRYPT_E_NO_REVOCATION_CHECK (0x80092012) | Le certificat vient d'une autorité interne qui ne publie pas de liste de révocation joignable ; Windows (schannel) refuse par défaut. | Le certificat est bien reconnu : curl.exe --ssl-no-revoke pour un test. Chrome n'est pas gêné. |
newman : erreur de certificat en HTTPS | newman tourne sous Node.js, qui ne lit pas le magasin de certificats de Windows : l'autorité interne lui est inconnue. | NODE_OPTIONS=--use-system-ca (Node.js 22 et plus). |
Clé API en clair dans le rapport newman | Le rapport HTML recopie les en-têtes des requêtes. | Masquer la clé dans les rapports avant de les partager. |
Sources
Consultées le 27 octobre 2026.
- About Node.js, OpenJS Foundation. Le modèle de Node.js : une boucle d'événements, des entrées-sorties non bloquantes.
- Node.js, Wikipédia (en). Ryan Dahl, 2009, présentation à la JSConf EU, moteur V8, npm en 2010, Node.js Foundation puis OpenJS Foundation.
- Node.js now available, RPGPGM.COM, décembre 2014. Node.js livré pour IBM i le 15 décembre 2014 : 5733-OPS option 1, prérequis PASE (5770-SS1 option 33), annoncé avec le TR9 de la 7.1 et le TR1 de la 7.2.
- Open source the Node.js port for IBM i, IBM Ideas. Réponse d'IBM (2018) : les modifications propres à IBM i sont dans le dépôt officiel de Node.js depuis la version 10.
- Node.js usage notes, IBM i OSS Docs. Installation par yum, choix d'une version paire LTS, changement de version par alternatives, paquets odbc, idb-connector, idb-pconnector et itoolkit.
- nodejs-idb-pconnector, IBM (GitHub). Le pool de connexions DBPool, prepareExecute et l'option enableNumericTypeConversion.
- nodejs-itoolkit, IBM (GitHub). ProgramCall et le transport idb vers XMLSERVICE.
- Fastify, OpenJS Foundation. Validation par schéma JSON, sérialisation, injection de requêtes pour les tests.
- Carbon Design System, IBM. Le design d'IBM repris par l'écran de test : couleurs, IBM Plex, composants.
Questions fréquentes
Quelle version de Node.js installer sur IBM i ?
La plus récente des versions paires en support long (LTS) proposées par le dépôt yum d'IBM. Le 27 octobre 2026, le dépôt ibmi-base proposait nodejs14 à nodejs24 : nodejs24 (24.22.1) a été installé. Les versions impaires ne sont pas faites pour la production.
Peut-on installer Node.js depuis une session 5250, sans SSH ?
Oui, dans QSH (STRQSH), si l'open source est déjà amorcé sur la machine. Il faut ajouter /QOpenSys/pkgs/bin au PATH (export PATH=/QOpenSys/pkgs/bin:$PATH), autoriser les threads avant de lancer QSH (ADDENVVAR ENVVAR(QIBM_MULTI_THREADED) VALUE(Y)), puis lancer yum -y install nodejs24 nodever avec un profil qui a *ALLOBJ. CALL QP2TERM fonctionne aussi.
Pourquoi yum répond « getaddrinfo() thread failed to start » ?
Parce que yum est lancé dans un travail QSH qui n'accepte pas plusieurs threads, alors que curl résout le nom du dépôt d'IBM dans un thread. Ce n'est pas un problème de réseau. Quitter QSH par F3, lancer ADDENVVAR ENVVAR(QIBM_MULTI_THREADED) VALUE(Y) REPLACE(*YES), puis STRQSH, ou passer par QP2TERM ou SSH.
Faut-il Apache ou l'HTTP Server d'IBM devant Node.js ?
Ce n'est pas obligatoire : Node.js écoute lui-même sur son port. Mais c'est la façon la plus simple d'avoir du HTTPS : une instance de l'IBM HTTP Server en reverse proxy chiffre sur le port 3443 avec un certificat DCM et relaie à Node.js, qui n'écoute plus que sur 127.0.0.1:3030.
Comment Node.js se connecte-t-il à Db2 for i ?
Avec idb-connector ou idb-pconnector, des paquets npm d'IBM qui appellent Db2 directement depuis PASE, dans le travail de Node.js : la connexion *LOCAL ne demande pas de mot de passe, elle a les droits du profil du travail. Pour une connexion depuis une autre machine, on prend le paquet odbc avec le pilote ODBC d'IBM i Access.
Peut-on appeler un programme RPG existant depuis Node.js ?
Oui, avec le paquet itoolkit, qui passe par XMLSERVICE (bibliothèque QXMLSERV, livrée avec le système). Le programme RPG n'est pas modifié : on décrit ses paramètres (15A, 7p0, 13p2...) et on récupère les valeurs de retour. Sur un système en français, les décimaux reviennent avec une virgule.
Sous quel profil faire tourner Node.js ?
Sous un profil de service dédié, sans mot de passe ni droit spécial, qui a seulement les droits utiles sur la bibliothèque de l'application. Dans les exemples, le profil NODEAPP a *CHANGE sur les tables et *USE sur le programme RPG et la procédure ; tout le reste de la bibliothèque est en *PUBLIC *EXCLUDE.
Comment lancer Node.js comme un service sur IBM i ?
Avec Service Commander (paquet service-commander) : un fichier YAML décrit la commande, le répertoire, le port à surveiller et les options du SBMJOB (file de travaux, profil). « sc start nodews » soumet alors un travail batch, « sc check nodews » vérifie qu'il écoute.