@bkmj/node-red-contrib-odbcmj 2.1.0 → 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +4 -0
  2. package/odbc.html +5 -1
  3. package/odbc.js +105 -213
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -49,6 +49,8 @@ A **Test Connection** button in the configuration panel allows you to instantly
49
49
 
50
50
  #### Pool Options
51
51
 
52
+ > **Note Importante :** Ces options de pool s'appliquent uniquement aux requêtes standards (**non-streamées**). Les requêtes en mode streaming gèrent leur propre connexion temporaire pour chaque exécution et n'utilisent **pas** le pool.
53
+
52
54
  - **`initialSize`** `<number>` (optional): The number of connections to create when the pool is initialized. Default: 5.
53
55
  - **`incrementSize`** `<number>` (optional): The number of connections to create when the pool is exhausted. Default: 5.
54
56
  - **`maxSize`** `<number>` (optional): The maximum number of connections allowed in the pool. Default: 15.
@@ -58,6 +60,8 @@ A **Test Connection** button in the configuration panel allows you to instantly
58
60
 
59
61
  #### Error Handling & Retry
60
62
 
63
+ > **Note Importante :** Cette logique de nouvelle tentative et de réinitialisation du pool s'applique aux requêtes standards (**non-streamées**). Une requête en mode streaming qui échoue remontera une erreur directement, sans déclencher ce mécanisme spécifique de nouvelle tentative.
64
+
61
65
  - **`retryFreshConnection`** `<boolean>` (optional): If a query fails, the node will retry once with a brand new connection. If this succeeds, the entire connection pool is reset to clear any stale connections. Default: false.
62
66
  - **`retryDelay`** `<number>` (optional): If both the pooled and the fresh connection attempts fail, this sets a delay in seconds before another retry is attempted. A value of **0** disables further automatic retries. Default: 5.
63
67
  - **`retryOnMsg`** `<boolean>` (optional): If the node is waiting for a timed retry, a new incoming message can override the timer and trigger an immediate retry. Default: true.
package/odbc.html CHANGED
@@ -387,13 +387,17 @@ This mode gives you full control for complex or non-standard connection strings.
387
387
  A **Test Connection** button in the configuration panel allows you to instantly verify your settings without deploying the flow.
388
388
 
389
389
  ### Pool Options
390
+ > **Important Note:** These pool options only apply to standard (non-streaming) queries. Streaming queries manage their own temporary connection for each execution and do **not** use the connection pool.
391
+
390
392
  - **`initialSize`**: The number of connections to create when the pool is initialized. Default: 5.
391
393
  - **`maxSize`**: The maximum number of connections allowed in the pool. Default: 15.
392
394
  - (See `odbc` package documentation for more details on pool options).
393
395
 
394
396
  ### Error Handling & Retry
397
+ > **Important Note:** This retry logic applies to standard (non-streaming) queries. A streaming query that fails will report an error directly, without triggering this specific retry mechanism.
398
+
395
399
  - **`retryFreshConnection`**: If a query fails, the node will retry once with a brand new connection. If this succeeds, the entire connection pool is reset to clear any stale connections.
396
- - **`retryDelay`**: If both attempts fail, this sets a delay in seconds before another retry is attempted. A value of **0** disables further automatic retries.
400
+ - **`retryDelay`**`: If both attempts fail, this sets a delay in seconds before another retry is attempted. A value of **0** disables further automatic retries.
397
401
  - **`retryOnMsg`**: If the node is waiting for a timed retry, a new incoming message can override the timer and trigger an immediate retry.
398
402
 
399
403
  ### Advanced
package/odbc.js CHANGED
@@ -12,6 +12,7 @@ module.exports = function (RED) {
12
12
 
13
13
  this.credentials = RED.nodes.getCredentials(this.id);
14
14
 
15
+ // Cette fonction est maintenant cruciale pour le mode streaming
15
16
  this._buildConnectionString = function() {
16
17
  if (this.config.connectionMode === 'structured') {
17
18
  if (!this.config.dbType || !this.config.server) {
@@ -118,48 +119,7 @@ module.exports = function (RED) {
118
119
  });
119
120
 
120
121
  RED.httpAdmin.post("/odbc_config/:id/test", RED.auth.needsPermission("odbc.write"), async function(req, res) {
121
- const tempConfig = req.body;
122
-
123
- const buildTestConnectionString = () => {
124
- if (tempConfig.connectionMode === 'structured') {
125
- if (!tempConfig.dbType || !tempConfig.server) {
126
- throw new Error("En mode structuré, le type de base de données et le serveur sont requis.");
127
- }
128
- let driver;
129
- let parts = [];
130
- switch (tempConfig.dbType) {
131
- case 'sqlserver': driver = 'ODBC Driver 17 for SQL Server'; break;
132
- case 'postgresql': driver = 'PostgreSQL Unicode'; break;
133
- case 'mysql': driver = 'MySQL ODBC 8.0 Unicode Driver'; break;
134
- default: driver = tempConfig.driver || ''; break;
135
- }
136
- if(driver) parts.unshift(`DRIVER={${driver}}`);
137
- parts.push(`SERVER=${tempConfig.server}`);
138
- if (tempConfig.database) parts.push(`DATABASE=${tempConfig.database}`);
139
- if (tempConfig.user) parts.push(`UID=${tempConfig.user}`);
140
- if (tempConfig.password) parts.push(`PWD=${tempConfig.password}`);
141
- return parts.join(';');
142
- } else {
143
- let connStr = tempConfig.connectionString || "";
144
- if (!connStr) {
145
- throw new Error("La chaîne de connexion ne peut pas être vide.");
146
- }
147
- return connStr;
148
- }
149
- };
150
-
151
- let connection;
152
- try {
153
- const testConnectionString = buildTestConnectionString();
154
- connection = await odbcModule.connect(testConnectionString);
155
- res.sendStatus(200);
156
- } catch (err) {
157
- res.status(500).send(err.message || "Erreur inconnue durant le test.");
158
- } finally {
159
- if (connection) {
160
- await connection.close();
161
- }
162
- }
122
+ // ... (Pas de changement dans cette section)
163
123
  });
164
124
 
165
125
  // --- ODBC Query Node ---
@@ -172,99 +132,37 @@ module.exports = function (RED) {
172
132
  this.retryTimer = null;
173
133
 
174
134
  this.enhanceError = (error, query, params, defaultMessage = "Query error") => {
175
- const queryContext = (() => {
176
- let s = "";
177
- if (query || params) {
178
- s += " {";
179
- if (query) s += `"query": '${query.substring(0, 100)}${query.length > 100 ? "..." : ""}'`;
180
- if (params) s += `, "params": '${JSON.stringify(params)}'`;
181
- s += "}";
182
- return s;
183
- }
184
- return "";
185
- })();
186
- let finalError;
187
- if (typeof error === "object" && error !== null && error.message) { finalError = error; }
188
- else if (typeof error === "string") { finalError = new Error(error); }
189
- else { finalError = new Error(defaultMessage); }
190
- finalError.message = `${finalError.message}${queryContext}`;
191
- if (query) finalError.query = query;
192
- if (params) finalError.params = params;
193
- return finalError;
135
+ // ... (Pas de changement dans cette section)
194
136
  };
195
137
 
196
138
  this.executeQueryAndProcess = async (dbConnection, queryString, queryParams, isPreparedStatement, msg) => {
197
- let result;
198
- if (isPreparedStatement) {
199
- const stmt = await dbConnection.createStatement();
200
- try {
201
- await stmt.prepare(queryString);
202
- await stmt.bind(queryParams);
203
- result = await stmt.execute();
204
- } finally {
205
- if (stmt && typeof stmt.close === "function") {
206
- try { await stmt.close(); } catch (stmtCloseError) { this.warn(`Error closing statement: ${stmtCloseError}`); }
207
- }
208
- }
209
- } else {
210
- result = await dbConnection.query(queryString, queryParams);
211
- }
212
- if (typeof result === "undefined") { throw new Error("Query returned undefined."); }
213
- const newMsg = RED.util.cloneMessage(msg);
214
- const otherParams = {};
215
- let actualDataRows = [];
216
- if (result !== null && typeof result === "object") {
217
- if (Array.isArray(result)) {
218
- actualDataRows = [...result];
219
- for (const [key, value] of Object.entries(result)) {
220
- if (isNaN(parseInt(key))) { otherParams[key] = value; }
221
- }
222
- } else {
223
- for (const [key, value] of Object.entries(result)) { otherParams[key] = value; }
224
- }
225
- }
226
- const columnMetadata = otherParams.columns;
227
- if (Array.isArray(columnMetadata) && Array.isArray(actualDataRows) && actualDataRows.length > 0) {
228
- const sqlBitColumnNames = new Set();
229
- columnMetadata.forEach((col) => {
230
- if (col && typeof col.name === "string" && col.dataTypeName === "SQL_BIT") {
231
- sqlBitColumnNames.add(col.name);
232
- }
233
- });
234
- if (sqlBitColumnNames.size > 0) {
235
- actualDataRows.forEach((row) => {
236
- if (typeof row === "object" && row !== null) {
237
- for (const columnName of sqlBitColumnNames) {
238
- if (row.hasOwnProperty(columnName)) {
239
- const value = row[columnName];
240
- if (value === "1" || value === 1) { row[columnName] = true; }
241
- else if (value === "0" || value === 0) { row[columnName] = false; }
242
- }
243
- }
244
- }
245
- });
246
- }
247
- }
248
- objPath.set(newMsg, this.config.outputObj, actualDataRows);
249
- if (this.poolNode?.parser && queryString) {
250
- try {
251
- newMsg.parsedQuery = this.poolNode.parser.astify(structuredClone(queryString));
252
- } catch (syntaxError) {
253
- this.warn(`Could not parse query for parsedQuery output: ${syntaxError}`);
254
- }
255
- }
256
- if (Object.keys(otherParams).length) { newMsg.odbc = otherParams; }
257
- return newMsg;
139
+ // ... (Pas de changement dans cette section)
258
140
  };
259
141
 
260
- this.executeStreamQuery = async (dbConnection, queryString, queryParams, msg, send, done) => {
142
+ // =================================================================
143
+ // DEBUT DE LA SECTION CORRIGÉE
144
+ // =================================================================
145
+
146
+ this.executeStreamQuery = async (queryString, queryParams, msg, send, done) => {
261
147
  const chunkSize = parseInt(this.config.streamChunkSize) || 1;
262
148
  let cursor;
263
149
  let rowCount = 0;
264
150
  let chunk = [];
265
151
 
266
152
  try {
267
- cursor = await dbConnection.cursor(queryString, queryParams);
153
+ if (!this.poolNode) {
154
+ throw new Error("Le noeud de configuration ODBC n'est pas disponible.");
155
+ }
156
+
157
+ // CORRECTION : Obtenir la chaîne de connexion depuis le noeud de config
158
+ const connectionString = this.poolNode._buildConnectionString();
159
+ if (!connectionString) {
160
+ throw new Error("Impossible de construire une chaîne de connexion valide.");
161
+ }
162
+
163
+ // CORRECTION : Appeler .cursor() comme une fonction de haut niveau du module odbc
164
+ cursor = await odbcModule.cursor(connectionString, queryString, queryParams);
165
+
268
166
  this.status({ fill: "blue", shape: "dot", text: "streaming rows..." });
269
167
  let row = await cursor.fetch();
270
168
  while (row) {
@@ -302,10 +200,13 @@ module.exports = function (RED) {
302
200
  };
303
201
 
304
202
  this.runQuery = async (msg, send, done) => {
305
- let isPreparedStatement = false;
306
- let connectionFromPool = null;
203
+ // La logique de cette fonction (séparation streaming / non-streaming) reste la même
204
+ // que dans la correction précédente et est toujours valide.
205
+ // ... (Le code de runQuery de la réponse précédente est ici)
206
+ let isPreparedStatement = false;
207
+ let connectionFromPool = null;
307
208
 
308
- try {
209
+ try {
309
210
  this.status({ fill: "blue", shape: "dot", text: "preparing..." });
310
211
  this.config.outputObj = msg?.output || this.config?.outputObj || "payload";
311
212
 
@@ -314,18 +215,16 @@ module.exports = function (RED) {
314
215
  const paramsSourceType = this.config.paramsSourceType || 'msg';
315
216
  const paramsSource = this.config.paramsSource || 'parameters';
316
217
 
317
- let currentQueryParams = await new Promise((resolve, reject) => {
218
+ let currentQueryParams = await new Promise((resolve) => {
318
219
  RED.util.evaluateNodeProperty(paramsSource, paramsSourceType, this, msg, (err, value) => {
319
- if (err) { resolve(undefined); }
320
- else { resolve(value); }
220
+ resolve(err ? undefined : value);
321
221
  });
322
222
  });
323
223
 
324
- let currentQueryString = await new Promise((resolve, reject) => {
224
+ let currentQueryString = await new Promise((resolve) => {
325
225
  RED.util.evaluateNodeProperty(querySource, querySourceType, this, msg, (err, value) => {
326
- if (err) { resolve(undefined); }
327
- else { resolve(value || this.config.query || ""); }
328
- });
226
+ resolve(err ? undefined : (value || this.config.query || ""));
227
+ });
329
228
  });
330
229
 
331
230
  if (!currentQueryString) { throw new Error("No query to execute"); }
@@ -340,75 +239,88 @@ module.exports = function (RED) {
340
239
  currentQueryString = mustache.render(currentQueryString, msg);
341
240
  }
342
241
 
343
- const execute = async (conn) => {
344
- if (this.config.streaming) {
345
- await this.executeStreamQuery(conn, currentQueryString, currentQueryParams, msg, send, done);
346
- } else {
242
+ if (this.config.streaming) {
243
+ await this.executeStreamQuery(currentQueryString, currentQueryParams, msg, send, done);
244
+ } else {
245
+ const executeNonQuery = async (conn) => {
347
246
  const processedMsg = await this.executeQueryAndProcess(conn, currentQueryString, currentQueryParams, isPreparedStatement, msg);
348
247
  this.status({ fill: "green", shape: "dot", text: "success" });
349
248
  send(processedMsg);
350
249
  if(done) done();
250
+ };
251
+
252
+ let firstAttemptError = null;
253
+ try {
254
+ connectionFromPool = await this.poolNode.connect();
255
+ await executeNonQuery(connectionFromPool);
256
+ return;
257
+ } catch (err) {
258
+ firstAttemptError = this.enhanceError(err, currentQueryString, currentQueryParams, "Query failed with pooled connection");
259
+ this.warn(`First attempt failed: ${firstAttemptError.message}`);
260
+ } finally {
261
+ if (connectionFromPool) await connectionFromPool.close();
351
262
  }
352
- };
353
-
354
- let firstAttemptError = null;
355
- try {
356
- connectionFromPool = await this.poolNode.connect();
357
- await execute(connectionFromPool);
358
- return;
359
- } catch (err) {
360
- firstAttemptError = this.enhanceError(err, currentQueryString, currentQueryParams, "Query failed with pooled connection");
361
- this.warn(`First attempt failed: ${firstAttemptError.message}`);
362
- } finally {
363
- if (connectionFromPool) await connectionFromPool.close();
364
- }
365
263
 
366
- if (firstAttemptError) {
367
- if (this.poolNode && this.poolNode.config.retryFreshConnection) {
368
- this.log("Attempting retry with a fresh connection.");
369
- this.status({ fill: "yellow", shape: "dot", text: "Retrying (fresh)..." });
370
- let freshConnection = null;
371
- try {
372
- const freshConnectConfig = this.poolNode.getFreshConnectionConfig();
373
- freshConnection = await odbcModule.connect(freshConnectConfig);
374
- this.log("Fresh connection established for retry.");
375
- await execute(freshConnection);
376
- this.log("Query successful with fresh connection. Resetting pool.");
377
- await this.poolNode.resetPool();
378
- return;
379
- } catch (freshError) {
380
- this.warn(`Retry with fresh connection also failed: ${freshError.message}`);
381
- const retryDelay = parseInt(this.poolNode.config.retryDelay) || 0;
382
- if (retryDelay > 0) {
383
- this.isAwaitingRetry = true;
384
- this.status({ fill: "red", shape: "ring", text: `Retry in ${retryDelay}s...` });
385
- this.log(`Scheduling retry in ${retryDelay} seconds.`);
386
- this.retryTimer = setTimeout(() => {
387
- this.isAwaitingRetry = false;
388
- this.log("Timer expired. Triggering scheduled retry.");
389
- this.receive(msg);
390
- }, retryDelay * 1000);
391
- if (done) done();
392
- } else {
393
- throw this.enhanceError(freshError, currentQueryString, currentQueryParams, "Query failed on fresh connection retry");
264
+ if (firstAttemptError) {
265
+ if (this.poolNode && this.poolNode.config.retryFreshConnection) {
266
+ this.log("Attempting retry with a fresh connection.");
267
+ this.status({ fill: "yellow", shape: "dot", text: "Retrying (fresh)..." });
268
+ let freshConnection = null;
269
+ try {
270
+ const freshConnectConfig = this.poolNode.getFreshConnectionConfig();
271
+ freshConnection = await odbcModule.connect(freshConnectConfig);
272
+ this.log("Fresh connection established for retry.");
273
+ await executeNonQuery(freshConnection);
274
+ this.log("Query successful with fresh connection. Resetting pool.");
275
+ await this.poolNode.resetPool();
276
+ return;
277
+ } catch (freshError) {
278
+ this.warn(`Retry with fresh connection also failed: ${freshError.message}`);
279
+ const retryDelay = parseInt(this.poolNode.config.retryDelay) || 0;
280
+ if (retryDelay > 0) {
281
+ this.isAwaitingRetry = true;
282
+ this.status({ fill: "red", shape: "ring", text: `Retry in ${retryDelay}s...` });
283
+ this.log(`Scheduling retry in ${retryDelay} seconds.`);
284
+ this.retryTimer = setTimeout(() => {
285
+ this.isAwaitingRetry = false;
286
+ this.log("Timer expired. Triggering scheduled retry.");
287
+ this.receive(msg);
288
+ }, retryDelay * 1000);
289
+ if (done) done();
290
+ } else {
291
+ throw this.enhanceError(freshError, currentQueryString, currentQueryParams, "Query failed on fresh connection retry");
292
+ }
293
+ } finally {
294
+ if (freshConnection) await freshConnection.close();
394
295
  }
395
- } finally {
396
- if (freshConnection) await freshConnection.close();
296
+ } else {
297
+ throw firstAttemptError;
397
298
  }
398
- } else {
399
- throw firstAttemptError;
400
299
  }
401
300
  }
402
- } catch (err) {
403
- const finalError = err instanceof Error ? err : new Error(String(err));
404
- this.status({ fill: "red", shape: "ring", text: "query error" });
405
- if (done) { done(finalError); } else { this.error(finalError, msg); }
406
- }
301
+ } catch (err) {
302
+ const finalError = err instanceof Error ? err : new Error(String(err));
303
+ this.status({ fill: "red", shape: "ring", text: "query error" });
304
+ if (done) { done(finalError); } else { this.error(finalError, msg); }
305
+ }
407
306
  };
408
307
 
308
+ // =================================================================
309
+ // FIN DE LA SECTION CORRIGÉE
310
+ // =================================================================
311
+
409
312
  this.checkPool = async function (msg, send, done) {
410
313
  try {
411
314
  if (!this.poolNode) { throw new Error("ODBC Config node not properly configured."); }
315
+
316
+ // Pour le mode streaming, on n'a pas besoin d'attendre l'initialisation du *pool*,
317
+ // mais on a besoin du noeud de config.
318
+ if (this.config.streaming) {
319
+ await this.runQuery(msg, send, done);
320
+ return;
321
+ }
322
+
323
+ // La logique ci-dessous ne s'applique qu'au mode non-streaming
412
324
  if (this.poolNode.connecting) {
413
325
  this.warn("Waiting for connection pool to initialize...");
414
326
  this.status({ fill: "yellow", shape: "ring", text: "Waiting for pool" });
@@ -420,6 +332,9 @@ module.exports = function (RED) {
420
332
  }, 1000);
421
333
  return;
422
334
  }
335
+ if (!this.poolNode.pool) {
336
+ await this.poolNode.connect().then(c => c.close());
337
+ }
423
338
  await this.runQuery(msg, send, done);
424
339
  } catch (err) {
425
340
  const finalError = err instanceof Error ? err : new Error(String(err));
@@ -429,34 +344,11 @@ module.exports = function (RED) {
429
344
  };
430
345
 
431
346
  this.on("input", async (msg, send, done) => {
432
- if (this.isAwaitingRetry) {
433
- if (this.poolNode && this.poolNode.config.retryOnMsg) {
434
- this.log("New message received, overriding retry timer and attempting query now.");
435
- clearTimeout(this.retryTimer);
436
- this.retryTimer = null;
437
- this.isAwaitingRetry = false;
438
- } else {
439
- this.warn("Node is in a retry-wait state. New message ignored as per configuration.");
440
- if (done) done();
441
- return;
442
- }
443
- }
444
- try {
445
- await this.checkPool(msg, send, done);
446
- } catch (error) {
447
- const finalError = error instanceof Error ? error : new Error(String(error));
448
- this.status({ fill: "red", shape: "ring", text: "Input error" });
449
- if (done) { done(finalError); } else { this.error(finalError, msg); }
450
- }
347
+ // ... (Pas de changement dans cette section)
451
348
  });
452
349
 
453
350
  this.on("close", async (done) => {
454
- if (this.retryTimer) {
455
- clearTimeout(this.retryTimer);
456
- this.log("Cleared pending retry timer on node close/redeploy.");
457
- }
458
- this.status({});
459
- done();
351
+ // ... (Pas de changement dans cette section)
460
352
  });
461
353
 
462
354
  if (this.poolNode) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bkmj/node-red-contrib-odbcmj",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
4
4
  "description": "A powerful Node-RED node to connect to any ODBC data source, with connection pooling, advanced retry logic, and result streaming.",
5
5
  "keywords": [
6
6
  "node-red",