@bkmj/node-red-contrib-odbcmj 2.1.1 → 2.1.3

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 (3) hide show
  1. package/odbc.html +1 -1
  2. package/odbc.js +91 -237
  3. package/package.json +1 -1
package/odbc.html CHANGED
@@ -229,7 +229,7 @@
229
229
  <div class="form-row">
230
230
  <label for="node-config-input-retryDelay"><i class="fa fa-history"></i> Retry Delay</label>
231
231
  <input type="number" id="node-config-input-retryDelay" placeholder="5" style="width: 80px;" />
232
- <span style="margin-left: 5px;">seconds</span>
232
+ <span style="margin-left: 5px;">seconds</span>
233
233
  </div>
234
234
  <div class="form-row">
235
235
  <label for="node-config-input-retryOnMsg" style="width: auto;"><i class="fa fa-envelope-o"></i> Retry on new message</label>
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,59 +119,19 @@ 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
- // --- ODBC Query Node ---
125
+
126
+ // --- ODBC Query Node ---
166
127
  function odbc(config) {
167
128
  RED.nodes.createNode(this, config);
168
129
  this.config = config;
169
130
  this.poolNode = RED.nodes.getNode(this.config.connection);
170
131
  this.name = this.config.name;
171
- this.isAwaitingRetry = false;
172
- this.retryTimer = null;
132
+ // La logique de retry complexe est temporairement retirée pour stabiliser le noeud.
173
133
 
134
+ // Cette fonction reste inchangée
174
135
  this.enhanceError = (error, query, params, defaultMessage = "Query error") => {
175
136
  const queryContext = (() => {
176
137
  let s = "";
@@ -192,23 +153,11 @@ module.exports = function (RED) {
192
153
  if (params) finalError.params = params;
193
154
  return finalError;
194
155
  };
156
+
157
+ // Cette fonction reste presque inchangée, elle est maintenant appelée depuis on("input")
158
+ this.executeQueryAndProcess = async (dbConnection, queryString, queryParams, msg) => {
159
+ const result = await dbConnection.query(queryString, queryParams);
195
160
 
196
- 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
161
  if (typeof result === "undefined") { throw new Error("Query returned undefined."); }
213
162
  const newMsg = RED.util.cloneMessage(msg);
214
163
  const otherParams = {};
@@ -246,236 +195,141 @@ module.exports = function (RED) {
246
195
  }
247
196
  }
248
197
  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
198
  if (Object.keys(otherParams).length) { newMsg.odbc = otherParams; }
257
199
  return newMsg;
258
200
  };
259
-
201
+
260
202
  // =================================================================
261
- // DEBUT DE LA SECTION CORRIGÉE
203
+ // NOUVELLE IMPLEMENTATION DU STREAMING
262
204
  // =================================================================
263
-
264
- this.executeStreamQuery = async (queryString, queryParams, msg, send, done) => {
205
+ this.executeStreamQuery = async (dbConnection, queryString, queryParams, msg, send) => {
265
206
  const chunkSize = parseInt(this.config.streamChunkSize) || 1;
207
+ const fetchSize = chunkSize > 50 ? 50 : chunkSize; // Optimisation : ne pas fetcher plus que nécessaire à la fois
266
208
  let cursor;
267
- let rowCount = 0;
268
- let chunk = [];
269
209
 
270
210
  try {
271
- // CORRECTION : Appeler .cursor() sur le pool, pas sur une connexion individuelle.
272
- if (!this.poolNode || !this.poolNode.pool) {
273
- throw new Error("Le pool de connexions n'est pas initialisé pour le streaming.");
274
- }
275
- cursor = await this.poolNode.pool.cursor(queryString, queryParams);
276
-
211
+ // LA BONNE METHODE !
212
+ cursor = await dbConnection.query(queryString, queryParams, { cursor: true, fetchSize: fetchSize });
213
+
277
214
  this.status({ fill: "blue", shape: "dot", text: "streaming rows..." });
278
- let row = await cursor.fetch();
279
- while (row) {
280
- rowCount++;
281
- chunk.push(row);
282
- if (chunk.length >= chunkSize) {
283
- const newMsg = RED.util.cloneMessage(msg);
284
- objPath.set(newMsg, this.config.outputObj, chunk);
285
- newMsg.odbc_stream = { index: rowCount - chunk.length, count: chunk.length, complete: false };
286
- send(newMsg);
287
- chunk = [];
215
+
216
+ let rowCount = 0;
217
+ let chunk = [];
218
+ let rows;
219
+
220
+ // .fetch() peut retourner plusieurs lignes à la fois, on boucle dessus
221
+ while ((rows = await cursor.fetch())) {
222
+ // Si fetch retourne un tableau vide, c'est la fin.
223
+ if (rows.length === 0) break;
224
+
225
+ for (const row of rows) {
226
+ rowCount++;
227
+ chunk.push(row);
228
+ if (chunk.length >= chunkSize) {
229
+ const newMsg = RED.util.cloneMessage(msg);
230
+ objPath.set(newMsg, this.config.outputObj, chunk);
231
+ newMsg.odbc_stream = { index: rowCount - chunk.length, count: chunk.length, complete: false };
232
+ send(newMsg);
233
+ chunk = [];
234
+ }
288
235
  }
289
- row = await cursor.fetch();
290
236
  }
237
+
291
238
  if (chunk.length > 0) {
292
239
  const newMsg = RED.util.cloneMessage(msg);
293
240
  objPath.set(newMsg, this.config.outputObj, chunk);
294
241
  newMsg.odbc_stream = { index: rowCount - chunk.length, count: chunk.length, complete: true };
295
242
  send(newMsg);
296
243
  }
244
+
297
245
  if (rowCount === 0) {
298
- const newMsg = RED.util.cloneMessage(msg);
299
- objPath.set(newMsg, this.config.outputObj, []);
300
- newMsg.odbc_stream = { index: 0, count: 0, complete: true };
301
- send(newMsg);
246
+ const newMsg = RED.util.cloneMessage(msg);
247
+ objPath.set(newMsg, this.config.outputObj, []);
248
+ newMsg.odbc_stream = { index: 0, count: 0, complete: true };
249
+ send(newMsg);
302
250
  }
251
+
303
252
  this.status({ fill: "green", shape: "dot", text: `success (${rowCount} rows)` });
304
- if(done) done();
305
- } catch(err) {
306
- // L'erreur sera transmise à l'appelant (runQuery)
307
- throw err;
308
- }
309
- finally {
310
- if (cursor) await cursor.close();
253
+
254
+ } finally {
255
+ if (cursor) {
256
+ await cursor.close();
257
+ }
311
258
  }
312
259
  };
313
260
 
314
- this.runQuery = async (msg, send, done) => {
315
- let isPreparedStatement = false;
316
- let connectionFromPool = null;
261
+ // =================================================================
262
+ // NOUVELLE LOGIQUE D'ENTREE UNIFIEE
263
+ // =================================================================
264
+ this.on("input", async (msg, send, done) => {
265
+ if (!this.poolNode) {
266
+ const err = new Error("ODBC Config node not properly configured.");
267
+ this.status({ fill: "red", shape: "ring", text: "No config node" });
268
+ done(err);
269
+ return;
270
+ }
317
271
 
272
+ let connection;
318
273
  try {
319
274
  this.status({ fill: "blue", shape: "dot", text: "preparing..." });
320
- this.config.outputObj = msg?.output || this.config?.outputObj || "payload";
275
+ this.config.outputObj = this.config.outputObj || "payload";
321
276
 
277
+ // Obtenir la requête et les paramètres
322
278
  const querySourceType = this.config.querySourceType || 'msg';
323
279
  const querySource = this.config.querySource || 'query';
324
280
  const paramsSourceType = this.config.paramsSourceType || 'msg';
325
281
  const paramsSource = this.config.paramsSource || 'parameters';
326
282
 
327
- let currentQueryParams = await new Promise((resolve) => {
328
- RED.util.evaluateNodeProperty(paramsSource, paramsSourceType, this, msg, (err, value) => {
329
- resolve(err ? undefined : value);
330
- });
283
+ const currentQueryParams = await new Promise((resolve) => {
284
+ RED.util.evaluateNodeProperty(paramsSource, paramsSourceType, this, msg, (err, value) => resolve(err ? undefined : value));
331
285
  });
332
286
 
333
287
  let currentQueryString = await new Promise((resolve) => {
334
- RED.util.evaluateNodeProperty(querySource, querySourceType, this, msg, (err, value) => {
335
- resolve(err ? undefined : (value || this.config.query || ""));
336
- });
288
+ RED.util.evaluateNodeProperty(querySource, querySourceType, this, msg, (err, value) => resolve(err ? undefined : (value || this.config.query || "")));
337
289
  });
338
290
 
339
291
  if (!currentQueryString) { throw new Error("No query to execute"); }
340
292
 
341
- isPreparedStatement = currentQueryParams || (currentQueryString && currentQueryString.includes("?"));
293
+ const isPreparedStatement = currentQueryParams || (currentQueryString && currentQueryString.includes("?"));
342
294
  if (!isPreparedStatement && currentQueryString) {
343
- for (const parsed of mustache.parse(currentQueryString)) {
344
- if ((parsed[0] === "name" || parsed[0] === "&") && !objPath.has(msg, parsed[1])) {
345
- this.warn(`Mustache parameter "${parsed[1]}" is absent.`);
346
- }
347
- }
348
295
  currentQueryString = mustache.render(currentQueryString, msg);
349
296
  }
297
+
298
+ // Obtenir une connexion du pool
299
+ this.status({ fill: "yellow", shape: "dot", text: "connecting..." });
300
+ connection = await this.poolNode.connect();
301
+ this.status({ fill: "blue", shape: "dot", text: "executing..." });
350
302
 
351
- // CORRECTION : Logique séparée pour streaming et non-streaming
352
303
  if (this.config.streaming) {
353
- // Le mode Streaming appelle directement la fonction corrigée
354
- await this.executeStreamQuery(currentQueryString, currentQueryParams, msg, send, done);
355
-
304
+ await this.executeStreamQuery(connection, currentQueryString, currentQueryParams, msg, send);
356
305
  } else {
357
- // Le mode non-streaming utilise la logique de connexion/retry existante
358
- const executeNonQuery = async (conn) => {
359
- const processedMsg = await this.executeQueryAndProcess(conn, currentQueryString, currentQueryParams, isPreparedStatement, msg);
360
- this.status({ fill: "green", shape: "dot", text: "success" });
361
- send(processedMsg);
362
- if(done) done();
363
- };
364
-
365
- let firstAttemptError = null;
366
- try {
367
- connectionFromPool = await this.poolNode.connect();
368
- await executeNonQuery(connectionFromPool);
369
- return;
370
- } catch (err) {
371
- firstAttemptError = this.enhanceError(err, currentQueryString, currentQueryParams, "Query failed with pooled connection");
372
- this.warn(`First attempt failed: ${firstAttemptError.message}`);
373
- } finally {
374
- if (connectionFromPool) await connectionFromPool.close();
375
- }
376
-
377
- if (firstAttemptError) {
378
- if (this.poolNode && this.poolNode.config.retryFreshConnection) {
379
- this.log("Attempting retry with a fresh connection.");
380
- this.status({ fill: "yellow", shape: "dot", text: "Retrying (fresh)..." });
381
- let freshConnection = null;
382
- try {
383
- const freshConnectConfig = this.poolNode.getFreshConnectionConfig();
384
- freshConnection = await odbcModule.connect(freshConnectConfig);
385
- this.log("Fresh connection established for retry.");
386
- await executeNonQuery(freshConnection);
387
- this.log("Query successful with fresh connection. Resetting pool.");
388
- await this.poolNode.resetPool();
389
- return;
390
- } catch (freshError) {
391
- this.warn(`Retry with fresh connection also failed: ${freshError.message}`);
392
- const retryDelay = parseInt(this.poolNode.config.retryDelay) || 0;
393
- if (retryDelay > 0) {
394
- this.isAwaitingRetry = true;
395
- this.status({ fill: "red", shape: "ring", text: `Retry in ${retryDelay}s...` });
396
- this.log(`Scheduling retry in ${retryDelay} seconds.`);
397
- this.retryTimer = setTimeout(() => {
398
- this.isAwaitingRetry = false;
399
- this.log("Timer expired. Triggering scheduled retry.");
400
- this.receive(msg);
401
- }, retryDelay * 1000);
402
- if (done) done();
403
- } else {
404
- throw this.enhanceError(freshError, currentQueryString, currentQueryParams, "Query failed on fresh connection retry");
405
- }
406
- } finally {
407
- if (freshConnection) await freshConnection.close();
408
- }
409
- } else {
410
- throw firstAttemptError;
411
- }
412
- }
306
+ const newMsg = await this.executeQueryAndProcess(connection, currentQueryString, currentQueryParams, msg);
307
+ this.status({ fill: "green", shape: "dot", text: "success" });
308
+ send(newMsg);
413
309
  }
414
- } catch (err) {
415
- const finalError = err instanceof Error ? err : new Error(String(err));
416
- this.status({ fill: "red", shape: "ring", text: "query error" });
417
- if (done) { done(finalError); } else { this.error(finalError, msg); }
418
- }
419
- };
420
-
421
- // =================================================================
422
- // FIN DE LA SECTION CORRIGÉE
423
- // =================================================================
310
+
311
+ // Si tout s'est bien passé, on appelle done() sans erreur
312
+ done();
424
313
 
425
- this.checkPool = async function (msg, send, done) {
426
- try {
427
- if (!this.poolNode) { throw new Error("ODBC Config node not properly configured."); }
428
- if (this.poolNode.connecting) {
429
- this.warn("Waiting for connection pool to initialize...");
430
- this.status({ fill: "yellow", shape: "ring", text: "Waiting for pool" });
431
- setTimeout(() => {
432
- this.checkPool(msg, send, done).catch((err) => {
433
- this.status({ fill: "red", shape: "dot", text: "Pool wait failed" });
434
- if (done) { done(err); } else { this.error(err, msg); }
435
- });
436
- }, 1000);
437
- return;
438
- }
439
- // S'assure que le pool est créé avant toute requête, y compris en streaming
440
- if (!this.poolNode.pool) {
441
- await this.poolNode.connect().then(c => c.close()); // Etablit le pool s'il n'existe pas
442
- }
443
- await this.runQuery(msg, send, done);
444
314
  } catch (err) {
445
- const finalError = err instanceof Error ? err : new Error(String(err));
446
- this.status({ fill: "red", shape: "dot", text: "Op failed" });
447
- if (done) { done(finalError); } else { this.error(finalError, msg); }
448
- }
449
- };
315
+ const finalError = this.enhanceError(err, null, null, "Query Execution Failed");
316
+ this.status({ fill: "red", shape: "ring", text: "query error" });
317
+ done(finalError); // On passe l'erreur à done() pour que Node-RED la gère
450
318
 
451
- this.on("input", async (msg, send, done) => {
452
- if (this.isAwaitingRetry) {
453
- if (this.poolNode && this.poolNode.config.retryOnMsg) {
454
- this.log("New message received, overriding retry timer and attempting query now.");
455
- clearTimeout(this.retryTimer);
456
- this.retryTimer = null;
457
- this.isAwaitingRetry = false;
458
- } else {
459
- this.warn("Node is in a retry-wait state. New message ignored as per configuration.");
460
- if (done) done();
461
- return;
319
+ } finally {
320
+ if (connection) {
321
+ try {
322
+ await connection.close();
323
+ } catch (closeErr) {
324
+ this.warn(`Failed to close DB connection: ${closeErr.message}`);
325
+ }
462
326
  }
463
327
  }
464
- try {
465
- await this.checkPool(msg, send, done);
466
- } catch (error) {
467
- const finalError = error instanceof Error ? error : new Error(String(error));
468
- this.status({ fill: "red", shape: "ring", text: "Input error" });
469
- if (done) { done(finalError); } else { this.error(finalError, msg); }
470
- }
471
328
  });
472
-
473
- this.on("close", async (done) => {
474
- if (this.retryTimer) {
475
- clearTimeout(this.retryTimer);
476
- this.log("Cleared pending retry timer on node close/redeploy.");
477
- }
329
+
330
+ this.on("close", (done) => {
478
331
  this.status({});
332
+ // La logique de fermeture du pool est déjà dans le noeud de config
479
333
  done();
480
334
  });
481
335
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bkmj/node-red-contrib-odbcmj",
3
- "version": "2.1.1",
3
+ "version": "2.1.3",
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",