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

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.
package/README.md CHANGED
@@ -105,4 +105,4 @@ When streaming is active, each output message will contain:
105
105
  - A `msg.odbc_stream` object with metadata for tracking progress:
106
106
  - `index`: The starting index of the current chunk (e.g., 0, 100, 200...).
107
107
  - `count`: The number of rows in the current chunk.
108
- - `complete`: A boolean that is `true` only on the very last message of the stream (or if the result set was empty), and `false` otherwise. This is useful for triggering a downstream action once all rows have been processed.
108
+ - `complete`: A boolean that is `true` only on the very last message, and `false` otherwise. The last payload will always be an empty array. This is useful for triggering a downstream action once all rows have been processed.
package/odbc.html CHANGED
@@ -430,5 +430,5 @@ When streaming is active, each output message will contain:
430
430
  - A `msg.odbc_stream` object with metadata for tracking progress:
431
431
  - `index`: The starting index of the current chunk (e.g., 0, 100, 200...).
432
432
  - `count`: The number of rows in the current chunk.
433
- - `complete`: A boolean that is `true` only on the very last message of the stream, useful for triggering a final action.
433
+ - `complete`: A boolean that is `true` only on the very last message of the stream, useful for triggering a final action. The last payload will always be an empty array.
434
434
  </script>
package/odbc.js CHANGED
@@ -122,16 +122,15 @@ module.exports = function (RED) {
122
122
  // ... (Pas de changement dans cette section)
123
123
  });
124
124
 
125
-
126
- // --- ODBC Query Node ---
125
+
126
+ // --- ODBC Query Node ---
127
127
  function odbc(config) {
128
128
  RED.nodes.createNode(this, config);
129
129
  this.config = config;
130
130
  this.poolNode = RED.nodes.getNode(this.config.connection);
131
131
  this.name = this.config.name;
132
- // La logique de retry complexe est temporairement retirée pour stabiliser le noeud.
133
132
 
134
- // Cette fonction reste inchangée
133
+ // ... (enhanceError et executeQueryAndProcess restent inchangés)
135
134
  this.enhanceError = (error, query, params, defaultMessage = "Query error") => {
136
135
  const queryContext = (() => {
137
136
  let s = "";
@@ -153,11 +152,9 @@ module.exports = function (RED) {
153
152
  if (params) finalError.params = params;
154
153
  return finalError;
155
154
  };
156
-
157
- // Cette fonction reste presque inchangée, elle est maintenant appelée depuis on("input")
155
+
158
156
  this.executeQueryAndProcess = async (dbConnection, queryString, queryParams, msg) => {
159
157
  const result = await dbConnection.query(queryString, queryParams);
160
-
161
158
  if (typeof result === "undefined") { throw new Error("Query returned undefined."); }
162
159
  const newMsg = RED.util.cloneMessage(msg);
163
160
  const otherParams = {};
@@ -199,29 +196,24 @@ module.exports = function (RED) {
199
196
  return newMsg;
200
197
  };
201
198
 
202
- // =================================================================
203
- // NOUVELLE IMPLEMENTATION DU STREAMING
204
- // =================================================================
205
199
  this.executeStreamQuery = async (dbConnection, queryString, queryParams, msg, send) => {
206
200
  const chunkSize = parseInt(this.config.streamChunkSize) || 1;
207
- const fetchSize = chunkSize > 50 ? 50 : chunkSize; // Optimisation : ne pas fetcher plus que nécessaire à la fois
201
+ const fetchSize = chunkSize > 100 ? 100 : chunkSize; // Optimisation du fetch
208
202
  let cursor;
209
-
203
+
210
204
  try {
211
- // LA BONNE METHODE !
212
205
  cursor = await dbConnection.query(queryString, queryParams, { cursor: true, fetchSize: fetchSize });
213
-
214
206
  this.status({ fill: "blue", shape: "dot", text: "streaming rows..." });
215
-
207
+
216
208
  let rowCount = 0;
217
209
  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
-
210
+
211
+ while (true) {
212
+ const rows = await cursor.fetch();
213
+ if (!rows || rows.length === 0) {
214
+ break;
215
+ }
216
+
225
217
  for (const row of rows) {
226
218
  rowCount++;
227
219
  chunk.push(row);
@@ -234,23 +226,21 @@ module.exports = function (RED) {
234
226
  }
235
227
  }
236
228
  }
237
-
229
+
238
230
  if (chunk.length > 0) {
239
231
  const newMsg = RED.util.cloneMessage(msg);
240
232
  objPath.set(newMsg, this.config.outputObj, chunk);
241
- newMsg.odbc_stream = { index: rowCount - chunk.length, count: chunk.length, complete: true };
242
- send(newMsg);
243
- }
244
-
245
- if (rowCount === 0) {
246
- const newMsg = RED.util.cloneMessage(msg);
247
- objPath.set(newMsg, this.config.outputObj, []);
248
- newMsg.odbc_stream = { index: 0, count: 0, complete: true };
233
+ newMsg.odbc_stream = { index: rowCount - chunk.length, count: chunk.length, complete: false };
249
234
  send(newMsg);
250
235
  }
251
-
236
+
237
+ const finalMsg = RED.util.cloneMessage(msg);
238
+ objPath.set(finalMsg, this.config.outputObj, []);
239
+ finalMsg.odbc_stream = { index: rowCount, count: 0, complete: true };
240
+ send(finalMsg);
241
+
252
242
  this.status({ fill: "green", shape: "dot", text: `success (${rowCount} rows)` });
253
-
243
+
254
244
  } finally {
255
245
  if (cursor) {
256
246
  await cursor.close();
@@ -258,78 +248,76 @@ module.exports = function (RED) {
258
248
  }
259
249
  };
260
250
 
261
- // =================================================================
262
- // NOUVELLE LOGIQUE D'ENTREE UNIFIEE
263
- // =================================================================
264
251
  this.on("input", async (msg, send, done) => {
265
252
  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;
253
+ return done(new Error("ODBC Config node not properly configured."));
270
254
  }
271
255
 
272
- let connection;
273
- try {
274
- this.status({ fill: "blue", shape: "dot", text: "preparing..." });
256
+ const execute = async (connection) => {
275
257
  this.config.outputObj = this.config.outputObj || "payload";
276
-
277
- // Obtenir la requête et les paramètres
258
+
278
259
  const querySourceType = this.config.querySourceType || 'msg';
279
260
  const querySource = this.config.querySource || 'query';
280
261
  const paramsSourceType = this.config.paramsSourceType || 'msg';
281
262
  const paramsSource = this.config.paramsSource || 'parameters';
282
-
283
- const currentQueryParams = await new Promise((resolve) => {
284
- RED.util.evaluateNodeProperty(paramsSource, paramsSourceType, this, msg, (err, value) => resolve(err ? undefined : value));
285
- });
286
-
287
- let currentQueryString = await new Promise((resolve) => {
288
- RED.util.evaluateNodeProperty(querySource, querySourceType, this, msg, (err, value) => resolve(err ? undefined : (value || this.config.query || "")));
289
- });
290
263
 
291
- if (!currentQueryString) { throw new Error("No query to execute"); }
264
+ const params = await new Promise(resolve => RED.util.evaluateNodeProperty(paramsSource, paramsSourceType, this, msg, (err, val) => resolve(err ? undefined : val)));
265
+ let query = await new Promise(resolve => RED.util.evaluateNodeProperty(querySource, querySourceType, this, msg, (err, val) => resolve(err ? undefined : (val || this.config.query || ""))));
292
266
 
293
- const isPreparedStatement = currentQueryParams || (currentQueryString && currentQueryString.includes("?"));
294
- if (!isPreparedStatement && currentQueryString) {
295
- currentQueryString = mustache.render(currentQueryString, msg);
267
+ if (!query) throw new Error("No query to execute");
268
+
269
+ const isPreparedStatement = params || (query && query.includes("?"));
270
+ if (!isPreparedStatement && query) {
271
+ query = mustache.render(query, msg);
296
272
  }
297
273
 
298
- // Obtenir une connexion du pool
299
- this.status({ fill: "yellow", shape: "dot", text: "connecting..." });
300
- connection = await this.poolNode.connect();
301
274
  this.status({ fill: "blue", shape: "dot", text: "executing..." });
302
-
303
275
  if (this.config.streaming) {
304
- await this.executeStreamQuery(connection, currentQueryString, currentQueryParams, msg, send);
276
+ await this.executeStreamQuery(connection, query, params, msg, send);
305
277
  } else {
306
- const newMsg = await this.executeQueryAndProcess(connection, currentQueryString, currentQueryParams, msg);
278
+ const newMsg = await this.executeQueryAndProcess(connection, query, params, msg);
307
279
  this.status({ fill: "green", shape: "dot", text: "success" });
308
280
  send(newMsg);
309
281
  }
310
-
311
- // Si tout s'est bien passé, on appelle done() sans erreur
312
- done();
313
-
314
- } catch (err) {
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
282
+ };
318
283
 
319
- } finally {
320
- if (connection) {
284
+ let connectionFromPool;
285
+ try {
286
+ this.status({ fill: "yellow", shape: "dot", text: "connecting..." });
287
+ connectionFromPool = await this.poolNode.connect();
288
+ await execute(connectionFromPool);
289
+ return done();
290
+ } catch (poolError) {
291
+ this.warn(`First attempt with pooled connection failed: ${poolError.message}`);
292
+ if (this.poolNode.config.retryFreshConnection) {
293
+ this.warn("Attempting retry with a fresh connection.");
294
+ this.status({ fill: "yellow", shape: "dot", text: "Retrying (fresh)..." });
295
+ let freshConnection;
321
296
  try {
322
- await connection.close();
323
- } catch (closeErr) {
324
- this.warn(`Failed to close DB connection: ${closeErr.message}`);
297
+ const freshConnectConfig = this.poolNode.getFreshConnectionConfig();
298
+ freshConnection = await odbcModule.connect(freshConnectConfig);
299
+ this.log("Fresh connection established for retry.");
300
+ await execute(freshConnection);
301
+ this.log("Query successful with fresh connection. Resetting pool.");
302
+ await this.poolNode.resetPool();
303
+ return done();
304
+ } catch (freshError) {
305
+ this.status({ fill: "red", shape: "ring", text: "retry failed" });
306
+ return done(this.enhanceError(freshError, null, null, "Retry with fresh connection also failed"));
307
+ } finally {
308
+ if (freshConnection) await freshConnection.close();
325
309
  }
310
+ } else {
311
+ this.status({ fill: "red", shape: "ring", text: "query error" });
312
+ return done(this.enhanceError(poolError));
326
313
  }
314
+ } finally {
315
+ if (connectionFromPool) await connectionFromPool.close();
327
316
  }
328
317
  });
329
318
 
330
319
  this.on("close", (done) => {
331
320
  this.status({});
332
- // La logique de fermeture du pool est déjà dans le noeud de config
333
321
  done();
334
322
  });
335
323
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bkmj/node-red-contrib-odbcmj",
3
- "version": "2.1.3",
3
+ "version": "2.2.0",
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",