@bkmj/node-red-contrib-odbcmj 2.1.4 → 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.
Files changed (4) hide show
  1. package/README.md +1 -1
  2. package/odbc.html +1 -1
  3. package/odbc.js +91 -110
  4. package/package.json +1 -1
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
@@ -123,15 +123,14 @@ module.exports = function (RED) {
123
123
  });
124
124
 
125
125
 
126
- // --- ODBC Query Node ---
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,144 +196,128 @@ 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
- const chunkSize = parseInt(this.config.streamChunkSize) || 1;
207
- // La taille du fetch peut être optimisée, mais restons simple pour la clarté.
208
- const fetchSize = chunkSize > 50 ? 50 : chunkSize;
209
- let cursor;
210
-
211
- try {
212
- cursor = await dbConnection.query(queryString, queryParams, { cursor: true, fetchSize: fetchSize });
213
- this.status({ fill: "blue", shape: "dot", text: "streaming rows..." });
214
-
215
- let rowCount = 0;
216
- let chunk = [];
217
-
218
- // Boucle infinie qui sera rompue de l'intérieur
219
- while (true) {
220
- const rows = await cursor.fetch();
221
-
222
- // 1. VÉRIFIER D'ABORD LA FIN DU FLUX
223
- if (!rows || rows.length === 0) {
224
- // Le flux de la base de données est terminé.
225
- // Le contenu actuel de `chunk` est le tout dernier lot.
226
- if (chunk.length > 0) {
227
- const newMsg = RED.util.cloneMessage(msg);
228
- objPath.set(newMsg, this.config.outputObj, chunk);
229
- // C'est le message final, donc `complete` est TRUE.
230
- newMsg.odbc_stream = { index: rowCount - chunk.length, count: chunk.length, complete: true };
231
- send(newMsg);
232
- } else if (rowCount === 0) {
233
- // Gérer le cas où la requête ne retourne aucune ligne.
234
- const newMsg = RED.util.cloneMessage(msg);
235
- objPath.set(newMsg, this.config.outputObj, []);
236
- newMsg.odbc_stream = { index: 0, count: 0, complete: true };
237
- send(newMsg);
200
+ const chunkSize = parseInt(this.config.streamChunkSize) || 1;
201
+ const fetchSize = chunkSize > 100 ? 100 : chunkSize; // Optimisation du fetch
202
+ let cursor;
203
+
204
+ try {
205
+ cursor = await dbConnection.query(queryString, queryParams, { cursor: true, fetchSize: fetchSize });
206
+ this.status({ fill: "blue", shape: "dot", text: "streaming rows..." });
207
+
208
+ let rowCount = 0;
209
+ let chunk = [];
210
+
211
+ while (true) {
212
+ const rows = await cursor.fetch();
213
+ if (!rows || rows.length === 0) {
214
+ break;
238
215
  }
239
- // Quitter la boucle car il n'y a plus rien à faire.
240
- break;
241
- }
242
-
243
- // 2. S'IL Y A DES LIGNES, LES TRAITER
244
- for (const row of rows) {
245
- rowCount++;
246
- chunk.push(row);
247
- if (chunk.length >= chunkSize) {
248
- const newMsg = RED.util.cloneMessage(msg);
249
- objPath.set(newMsg, this.config.outputObj, chunk);
250
- // Ce lot n'est pas le dernier, donc `complete` est FALSE.
251
- newMsg.odbc_stream = { index: rowCount - chunk.length, count: chunk.length, complete: false };
252
- send(newMsg);
253
- // Vider le lot pour le prochain remplissage.
254
- chunk = [];
216
+
217
+ for (const row of rows) {
218
+ rowCount++;
219
+ chunk.push(row);
220
+ if (chunk.length >= chunkSize) {
221
+ const newMsg = RED.util.cloneMessage(msg);
222
+ objPath.set(newMsg, this.config.outputObj, chunk);
223
+ newMsg.odbc_stream = { index: rowCount - chunk.length, count: chunk.length, complete: false };
224
+ send(newMsg);
225
+ chunk = [];
226
+ }
255
227
  }
256
228
  }
257
- } // Fin de la boucle while
258
-
259
- this.status({ fill: "green", shape: "dot", text: `success (${rowCount} rows)` });
260
-
261
- } finally {
262
- if (cursor) {
263
- await cursor.close();
229
+
230
+ if (chunk.length > 0) {
231
+ const newMsg = RED.util.cloneMessage(msg);
232
+ objPath.set(newMsg, this.config.outputObj, chunk);
233
+ newMsg.odbc_stream = { index: rowCount - chunk.length, count: chunk.length, complete: false };
234
+ send(newMsg);
235
+ }
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
+
242
+ this.status({ fill: "green", shape: "dot", text: `success (${rowCount} rows)` });
243
+
244
+ } finally {
245
+ if (cursor) {
246
+ await cursor.close();
247
+ }
264
248
  }
265
- }
266
- };
249
+ };
267
250
 
268
- // =================================================================
269
- // NOUVELLE LOGIQUE D'ENTREE UNIFIEE
270
- // =================================================================
271
251
  this.on("input", async (msg, send, done) => {
272
252
  if (!this.poolNode) {
273
- const err = new Error("ODBC Config node not properly configured.");
274
- this.status({ fill: "red", shape: "ring", text: "No config node" });
275
- done(err);
276
- return;
253
+ return done(new Error("ODBC Config node not properly configured."));
277
254
  }
278
255
 
279
- let connection;
280
- try {
281
- this.status({ fill: "blue", shape: "dot", text: "preparing..." });
256
+ const execute = async (connection) => {
282
257
  this.config.outputObj = this.config.outputObj || "payload";
283
-
284
- // Obtenir la requête et les paramètres
258
+
285
259
  const querySourceType = this.config.querySourceType || 'msg';
286
260
  const querySource = this.config.querySource || 'query';
287
261
  const paramsSourceType = this.config.paramsSourceType || 'msg';
288
262
  const paramsSource = this.config.paramsSource || 'parameters';
289
-
290
- const currentQueryParams = await new Promise((resolve) => {
291
- RED.util.evaluateNodeProperty(paramsSource, paramsSourceType, this, msg, (err, value) => resolve(err ? undefined : value));
292
- });
293
-
294
- let currentQueryString = await new Promise((resolve) => {
295
- RED.util.evaluateNodeProperty(querySource, querySourceType, this, msg, (err, value) => resolve(err ? undefined : (value || this.config.query || "")));
296
- });
297
263
 
298
- 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 || ""))));
299
266
 
300
- const isPreparedStatement = currentQueryParams || (currentQueryString && currentQueryString.includes("?"));
301
- if (!isPreparedStatement && currentQueryString) {
302
- 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);
303
272
  }
304
273
 
305
- // Obtenir une connexion du pool
306
- this.status({ fill: "yellow", shape: "dot", text: "connecting..." });
307
- connection = await this.poolNode.connect();
308
274
  this.status({ fill: "blue", shape: "dot", text: "executing..." });
309
-
310
275
  if (this.config.streaming) {
311
- await this.executeStreamQuery(connection, currentQueryString, currentQueryParams, msg, send);
276
+ await this.executeStreamQuery(connection, query, params, msg, send);
312
277
  } else {
313
- const newMsg = await this.executeQueryAndProcess(connection, currentQueryString, currentQueryParams, msg);
278
+ const newMsg = await this.executeQueryAndProcess(connection, query, params, msg);
314
279
  this.status({ fill: "green", shape: "dot", text: "success" });
315
280
  send(newMsg);
316
281
  }
317
-
318
- // Si tout s'est bien passé, on appelle done() sans erreur
319
- done();
320
-
321
- } catch (err) {
322
- const finalError = this.enhanceError(err, null, null, "Query Execution Failed");
323
- this.status({ fill: "red", shape: "ring", text: "query error" });
324
- done(finalError); // On passe l'erreur à done() pour que Node-RED la gère
282
+ };
325
283
 
326
- } finally {
327
- 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;
328
296
  try {
329
- await connection.close();
330
- } catch (closeErr) {
331
- 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();
332
309
  }
310
+ } else {
311
+ this.status({ fill: "red", shape: "ring", text: "query error" });
312
+ return done(this.enhanceError(poolError));
333
313
  }
314
+ } finally {
315
+ if (connectionFromPool) await connectionFromPool.close();
334
316
  }
335
317
  });
336
318
 
337
319
  this.on("close", (done) => {
338
320
  this.status({});
339
- // La logique de fermeture du pool est déjà dans le noeud de config
340
321
  done();
341
322
  });
342
323
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bkmj/node-red-contrib-odbcmj",
3
- "version": "2.1.4",
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",