@bkmj/node-red-contrib-odbcmj 2.1.2 → 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 (4) hide show
  1. package/README.md +0 -4
  2. package/odbc.html +2 -6
  3. package/odbc.js +149 -167
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -49,8 +49,6 @@ 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
-
54
52
  - **`initialSize`** `<number>` (optional): The number of connections to create when the pool is initialized. Default: 5.
55
53
  - **`incrementSize`** `<number>` (optional): The number of connections to create when the pool is exhausted. Default: 5.
56
54
  - **`maxSize`** `<number>` (optional): The maximum number of connections allowed in the pool. Default: 15.
@@ -60,8 +58,6 @@ A **Test Connection** button in the configuration panel allows you to instantly
60
58
 
61
59
  #### Error Handling & Retry
62
60
 
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
-
65
61
  - **`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.
66
62
  - **`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.
67
63
  - **`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
@@ -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>
@@ -387,17 +387,13 @@ 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
-
392
390
  - **`initialSize`**: The number of connections to create when the pool is initialized. Default: 5.
393
391
  - **`maxSize`**: The maximum number of connections allowed in the pool. Default: 15.
394
392
  - (See `odbc` package documentation for more details on pool options).
395
393
 
396
394
  ### 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
-
399
395
  - **`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.
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.
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.
401
397
  - **`retryOnMsg`**: If the node is waiting for a timed retry, a new incoming message can override the timer and trigger an immediate retry.
402
398
 
403
399
  ### Advanced
package/odbc.js CHANGED
@@ -122,233 +122,215 @@ module.exports = function (RED) {
122
122
  // ... (Pas de changement dans cette section)
123
123
  });
124
124
 
125
- // --- ODBC Query Node ---
125
+
126
+ // --- ODBC Query Node ---
126
127
  function odbc(config) {
127
128
  RED.nodes.createNode(this, config);
128
129
  this.config = config;
129
130
  this.poolNode = RED.nodes.getNode(this.config.connection);
130
131
  this.name = this.config.name;
131
- this.isAwaitingRetry = false;
132
- this.retryTimer = null;
132
+ // La logique de retry complexe est temporairement retirée pour stabiliser le noeud.
133
133
 
134
+ // Cette fonction reste inchangée
134
135
  this.enhanceError = (error, query, params, defaultMessage = "Query error") => {
135
- // ... (Pas de changement dans cette section)
136
+ const queryContext = (() => {
137
+ let s = "";
138
+ if (query || params) {
139
+ s += " {";
140
+ if (query) s += `"query": '${query.substring(0, 100)}${query.length > 100 ? "..." : ""}'`;
141
+ if (params) s += `, "params": '${JSON.stringify(params)}'`;
142
+ s += "}";
143
+ return s;
144
+ }
145
+ return "";
146
+ })();
147
+ let finalError;
148
+ if (typeof error === "object" && error !== null && error.message) { finalError = error; }
149
+ else if (typeof error === "string") { finalError = new Error(error); }
150
+ else { finalError = new Error(defaultMessage); }
151
+ finalError.message = `${finalError.message}${queryContext}`;
152
+ if (query) finalError.query = query;
153
+ if (params) finalError.params = params;
154
+ return finalError;
136
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);
137
160
 
138
- this.executeQueryAndProcess = async (dbConnection, queryString, queryParams, isPreparedStatement, msg) => {
139
- // ... (Pas de changement dans cette section)
161
+ if (typeof result === "undefined") { throw new Error("Query returned undefined."); }
162
+ const newMsg = RED.util.cloneMessage(msg);
163
+ const otherParams = {};
164
+ let actualDataRows = [];
165
+ if (result !== null && typeof result === "object") {
166
+ if (Array.isArray(result)) {
167
+ actualDataRows = [...result];
168
+ for (const [key, value] of Object.entries(result)) {
169
+ if (isNaN(parseInt(key))) { otherParams[key] = value; }
170
+ }
171
+ } else {
172
+ for (const [key, value] of Object.entries(result)) { otherParams[key] = value; }
173
+ }
174
+ }
175
+ const columnMetadata = otherParams.columns;
176
+ if (Array.isArray(columnMetadata) && Array.isArray(actualDataRows) && actualDataRows.length > 0) {
177
+ const sqlBitColumnNames = new Set();
178
+ columnMetadata.forEach((col) => {
179
+ if (col && typeof col.name === "string" && col.dataTypeName === "SQL_BIT") {
180
+ sqlBitColumnNames.add(col.name);
181
+ }
182
+ });
183
+ if (sqlBitColumnNames.size > 0) {
184
+ actualDataRows.forEach((row) => {
185
+ if (typeof row === "object" && row !== null) {
186
+ for (const columnName of sqlBitColumnNames) {
187
+ if (row.hasOwnProperty(columnName)) {
188
+ const value = row[columnName];
189
+ if (value === "1" || value === 1) { row[columnName] = true; }
190
+ else if (value === "0" || value === 0) { row[columnName] = false; }
191
+ }
192
+ }
193
+ }
194
+ });
195
+ }
196
+ }
197
+ objPath.set(newMsg, this.config.outputObj, actualDataRows);
198
+ if (Object.keys(otherParams).length) { newMsg.odbc = otherParams; }
199
+ return newMsg;
140
200
  };
141
-
201
+
142
202
  // =================================================================
143
- // DEBUT DE LA SECTION CORRIGÉE
203
+ // NOUVELLE IMPLEMENTATION DU STREAMING
144
204
  // =================================================================
145
-
146
- this.executeStreamQuery = async (queryString, queryParams, msg, send, done) => {
205
+ this.executeStreamQuery = async (dbConnection, queryString, queryParams, msg, send) => {
147
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
148
208
  let cursor;
149
- let rowCount = 0;
150
- let chunk = [];
151
209
 
152
210
  try {
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
-
211
+ // LA BONNE METHODE !
212
+ cursor = await dbConnection.query(queryString, queryParams, { cursor: true, fetchSize: fetchSize });
213
+
166
214
  this.status({ fill: "blue", shape: "dot", text: "streaming rows..." });
167
- let row = await cursor.fetch();
168
- while (row) {
169
- rowCount++;
170
- chunk.push(row);
171
- if (chunk.length >= chunkSize) {
172
- const newMsg = RED.util.cloneMessage(msg);
173
- objPath.set(newMsg, this.config.outputObj, chunk);
174
- newMsg.odbc_stream = { index: rowCount - chunk.length, count: chunk.length, complete: false };
175
- send(newMsg);
176
- 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
+ }
177
235
  }
178
- row = await cursor.fetch();
179
236
  }
237
+
180
238
  if (chunk.length > 0) {
181
239
  const newMsg = RED.util.cloneMessage(msg);
182
240
  objPath.set(newMsg, this.config.outputObj, chunk);
183
241
  newMsg.odbc_stream = { index: rowCount - chunk.length, count: chunk.length, complete: true };
184
242
  send(newMsg);
185
243
  }
244
+
186
245
  if (rowCount === 0) {
187
- const newMsg = RED.util.cloneMessage(msg);
188
- objPath.set(newMsg, this.config.outputObj, []);
189
- newMsg.odbc_stream = { index: 0, count: 0, complete: true };
190
- 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);
191
250
  }
251
+
192
252
  this.status({ fill: "green", shape: "dot", text: `success (${rowCount} rows)` });
193
- if(done) done();
194
- } catch(err) {
195
- throw err;
196
- }
197
- finally {
198
- if (cursor) await cursor.close();
253
+
254
+ } finally {
255
+ if (cursor) {
256
+ await cursor.close();
257
+ }
199
258
  }
200
259
  };
201
260
 
202
- this.runQuery = async (msg, send, done) => {
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;
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
+ }
208
271
 
272
+ let connection;
209
273
  try {
210
274
  this.status({ fill: "blue", shape: "dot", text: "preparing..." });
211
- this.config.outputObj = msg?.output || this.config?.outputObj || "payload";
275
+ this.config.outputObj = this.config.outputObj || "payload";
212
276
 
277
+ // Obtenir la requête et les paramètres
213
278
  const querySourceType = this.config.querySourceType || 'msg';
214
279
  const querySource = this.config.querySource || 'query';
215
280
  const paramsSourceType = this.config.paramsSourceType || 'msg';
216
281
  const paramsSource = this.config.paramsSource || 'parameters';
217
282
 
218
- let currentQueryParams = await new Promise((resolve) => {
219
- RED.util.evaluateNodeProperty(paramsSource, paramsSourceType, this, msg, (err, value) => {
220
- resolve(err ? undefined : value);
221
- });
283
+ const currentQueryParams = await new Promise((resolve) => {
284
+ RED.util.evaluateNodeProperty(paramsSource, paramsSourceType, this, msg, (err, value) => resolve(err ? undefined : value));
222
285
  });
223
286
 
224
287
  let currentQueryString = await new Promise((resolve) => {
225
- RED.util.evaluateNodeProperty(querySource, querySourceType, this, msg, (err, value) => {
226
- resolve(err ? undefined : (value || this.config.query || ""));
227
- });
288
+ RED.util.evaluateNodeProperty(querySource, querySourceType, this, msg, (err, value) => resolve(err ? undefined : (value || this.config.query || "")));
228
289
  });
229
290
 
230
291
  if (!currentQueryString) { throw new Error("No query to execute"); }
231
292
 
232
- isPreparedStatement = currentQueryParams || (currentQueryString && currentQueryString.includes("?"));
293
+ const isPreparedStatement = currentQueryParams || (currentQueryString && currentQueryString.includes("?"));
233
294
  if (!isPreparedStatement && currentQueryString) {
234
- for (const parsed of mustache.parse(currentQueryString)) {
235
- if ((parsed[0] === "name" || parsed[0] === "&") && !objPath.has(msg, parsed[1])) {
236
- this.warn(`Mustache parameter "${parsed[1]}" is absent.`);
237
- }
238
- }
239
295
  currentQueryString = mustache.render(currentQueryString, msg);
240
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..." });
241
302
 
242
303
  if (this.config.streaming) {
243
- await this.executeStreamQuery(currentQueryString, currentQueryParams, msg, send, done);
304
+ await this.executeStreamQuery(connection, currentQueryString, currentQueryParams, msg, send);
244
305
  } else {
245
- const executeNonQuery = async (conn) => {
246
- const processedMsg = await this.executeQueryAndProcess(conn, currentQueryString, currentQueryParams, isPreparedStatement, msg);
247
- this.status({ fill: "green", shape: "dot", text: "success" });
248
- send(processedMsg);
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();
262
- }
263
-
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();
295
- }
296
- } else {
297
- throw firstAttemptError;
298
- }
299
- }
306
+ const newMsg = await this.executeQueryAndProcess(connection, currentQueryString, currentQueryParams, msg);
307
+ this.status({ fill: "green", shape: "dot", text: "success" });
308
+ send(newMsg);
300
309
  }
310
+
311
+ // Si tout s'est bien passé, on appelle done() sans erreur
312
+ done();
313
+
301
314
  } catch (err) {
302
- const finalError = err instanceof Error ? err : new Error(String(err));
315
+ const finalError = this.enhanceError(err, null, null, "Query Execution Failed");
303
316
  this.status({ fill: "red", shape: "ring", text: "query error" });
304
- if (done) { done(finalError); } else { this.error(finalError, msg); }
305
- }
306
- };
317
+ done(finalError); // On passe l'erreur à done() pour que Node-RED la gère
307
318
 
308
- // =================================================================
309
- // FIN DE LA SECTION CORRIGÉE
310
- // =================================================================
311
-
312
- this.checkPool = async function (msg, send, done) {
313
- try {
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
324
- if (this.poolNode.connecting) {
325
- this.warn("Waiting for connection pool to initialize...");
326
- this.status({ fill: "yellow", shape: "ring", text: "Waiting for pool" });
327
- setTimeout(() => {
328
- this.checkPool(msg, send, done).catch((err) => {
329
- this.status({ fill: "red", shape: "dot", text: "Pool wait failed" });
330
- if (done) { done(err); } else { this.error(err, msg); }
331
- });
332
- }, 1000);
333
- return;
334
- }
335
- if (!this.poolNode.pool) {
336
- await this.poolNode.connect().then(c => c.close());
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
+ }
337
326
  }
338
- await this.runQuery(msg, send, done);
339
- } catch (err) {
340
- const finalError = err instanceof Error ? err : new Error(String(err));
341
- this.status({ fill: "red", shape: "dot", text: "Op failed" });
342
- if (done) { done(finalError); } else { this.error(finalError, msg); }
343
327
  }
344
- };
345
-
346
- this.on("input", async (msg, send, done) => {
347
- // ... (Pas de changement dans cette section)
348
328
  });
349
-
350
- this.on("close", async (done) => {
351
- // ... (Pas de changement dans cette section)
329
+
330
+ this.on("close", (done) => {
331
+ this.status({});
332
+ // La logique de fermeture du pool est déjà dans le noeud de config
333
+ done();
352
334
  });
353
335
 
354
336
  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.2",
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",