@bkmj/node-red-contrib-odbcmj 2.1.4 → 2.2.1

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 +40 -10
  3. package/odbc.js +149 -118
  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
@@ -71,12 +71,36 @@
71
71
 
72
72
  $('#node-config-test-connection').on('click', function() {
73
73
  var button = $(this);
74
+ var connectionMode = $("#node-config-input-connectionMode").val();
75
+
76
+ if (connectionMode === 'structured') {
77
+ var server = $("#node-config-input-server").val().trim();
78
+ if (!server) {
79
+ RED.notify("Le champ 'Server' est requis pour le test.", {type: "warning", timeout: 3000});
80
+ return;
81
+ }
82
+ } else {
83
+ var connStr = $("#node-config-input-connectionString").val().trim();
84
+ if (!connStr) {
85
+ RED.notify("La chaîne de connexion est requise pour le test.", {type: "warning", timeout: 3000});
86
+ return;
87
+ }
88
+ // Nouvelle validation :
89
+ // Elle est invalide SEULEMENT si elle ne contient PAS de DSN ET qu'elle ne respecte PAS l'ancien critère pour les chaînes DSN-less.
90
+ var isDsnString = /DSN=[^;]+/i.test(connStr);
91
+ var isDriverBasedString = /DRIVER=\{.+?\}/i.test(connStr) && /(SERVER|DATABASE|UID|PWD)=/i.test(connStr); // Un peu plus complet
92
+
93
+ if (!isDsnString && !isDriverBasedString) {
94
+ RED.notify("La chaîne de connexion semble invalide ou incomplète (ex: DSN=value; ou DRIVER={...};SERVER=...;).", {type: "warning", timeout: 4000});
95
+ return;
96
+ }
97
+ }
98
+
74
99
  var originalText = "Test Connection";
75
100
  var icon = button.find("i");
76
101
  icon.removeClass('fa-bolt').addClass('fa-spinner fa-spin');
77
102
  button.text(' Testing...').prop('disabled', true);
78
103
 
79
- var connectionMode = $("#node-config-input-connectionMode").val();
80
104
  var configData = {
81
105
  connectionMode: connectionMode,
82
106
  dbType: $("#node-config-input-dbType").val(),
@@ -272,7 +296,6 @@
272
296
  outputObj: { value: "payload" },
273
297
  streaming: { value: false },
274
298
  streamChunkSize: { value: 1, validate: RED.validators.number() },
275
- // Nouveaux `defaults` pour les Typed Inputs
276
299
  querySource: { value: "query", required: false },
277
300
  querySourceType: { value: "msg", required: false },
278
301
  paramsSource: { value: "parameters", required: false },
@@ -295,7 +318,6 @@
295
318
  $(".stream-options").toggle(this.checked);
296
319
  }).trigger("change");
297
320
 
298
- // Initialisation des Typed Inputs
299
321
  $("#node-input-querySource").typedInput({
300
322
  default: 'msg',
301
323
  typeField: "#node-input-querySourceType",
@@ -414,7 +436,7 @@ Executes a query against a configured ODBC data source.
414
436
  ### Properties
415
437
 
416
438
  - **Connection**: The `odbc config` node to use.
417
- - **Query (fallback)**: A static SQL query to run if the "Query Source" does not provide one. Can contain Mustache syntax (e.g., `{{{payload.id}}}`).
439
+ - **Query (fallback)**: A static SQL query to run if the "Query (fallback)" does not provide one. Can contain Mustache syntax (e.g., `{{{payload.id}}}`).
418
440
  - **Result to**: The `msg` property where the query result will be stored. Default: `payload`.
419
441
 
420
442
  ### Streaming Results
@@ -425,10 +447,18 @@ For queries that return a large number of rows, streaming prevents high memory u
425
447
  - **`Chunk Size`**: The number of rows to include in each output message. A value of `1` means one message will be sent for every single row.
426
448
 
427
449
  #### Streaming Output Format
428
- When streaming is active, each output message will contain:
429
- - A payload (or the configured output property) containing an array of rows for the current chunk.
430
- - A `msg.odbc_stream` object with metadata for tracking progress:
431
- - `index`: The starting index of the current chunk (e.g., 0, 100, 200...).
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.
450
+ When streaming is active, the node sends messages in sequence:
451
+
452
+ 1. **Data Messages**: One or more messages where the payload (or the configured output property) contains an array of rows for the current chunk. For these messages, `msg.odbc_stream.complete` will be **`false`**.
453
+
454
+ 2. **Completion Message**: A single, final message indicating the end of the stream. For this message:
455
+ - The payload will be an **empty array `[]`**.
456
+ - `msg.odbc_stream.complete` will be **`true`**.
457
+
458
+ The `msg.odbc_stream` object contains metadata for tracking:
459
+ - `index`: The starting index of the current chunk. For the completion message, this will be the total number of rows processed.
460
+ - `count`: The number of rows in the chunk. This will be `0` for the completion message.
461
+ - `complete`: The boolean flag (`true`/`false`).
462
+
463
+ This pattern ensures you can reliably trigger a final action (like closing a file or calculating an aggregate) only when the message with `complete: true` is received.
434
464
  </script>
package/odbc.js CHANGED
@@ -12,7 +12,6 @@ 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
16
15
  this._buildConnectionString = function() {
17
16
  if (this.config.connectionMode === 'structured') {
18
17
  if (!this.config.dbType || !this.config.server) {
@@ -119,19 +118,69 @@ module.exports = function (RED) {
119
118
  });
120
119
 
121
120
  RED.httpAdmin.post("/odbc_config/:id/test", RED.auth.needsPermission("odbc.write"), async function(req, res) {
122
- // ... (Pas de changement dans cette section)
123
- });
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
+
155
+ // ==============================================================
156
+ // LIGNE DE DÉBOGAGE AJOUTÉE
157
+ // ==============================================================
158
+ console.log("[ODBC Test] Attempting to connect with string:", testConnectionString);
159
+ // ==============================================================
124
160
 
161
+ const connectionOptions = {
162
+ connectionString: testConnectionString,
163
+ loginTimeout: 10
164
+ };
165
+ connection = await odbcModule.connect(connectionOptions);
166
+ res.sendStatus(200);
167
+ } catch (err) {
168
+ console.error("[ODBC Test] Connection failed:", err); // Ajout d'un log d'erreur
169
+ res.status(500).send(err.message || "Erreur inconnue durant le test.");
170
+ } finally {
171
+ if (connection) {
172
+ await connection.close();
173
+ }
174
+ }
175
+ });
125
176
 
126
- // --- ODBC Query Node ---
177
+ // --- ODBC Query Node ---
127
178
  function odbc(config) {
128
179
  RED.nodes.createNode(this, config);
129
180
  this.config = config;
130
181
  this.poolNode = RED.nodes.getNode(this.config.connection);
131
182
  this.name = this.config.name;
132
- // La logique de retry complexe est temporairement retirée pour stabiliser le noeud.
133
183
 
134
- // Cette fonction reste inchangée
135
184
  this.enhanceError = (error, query, params, defaultMessage = "Query error") => {
136
185
  const queryContext = (() => {
137
186
  let s = "";
@@ -153,11 +202,9 @@ module.exports = function (RED) {
153
202
  if (params) finalError.params = params;
154
203
  return finalError;
155
204
  };
156
-
157
- // Cette fonction reste presque inchangée, elle est maintenant appelée depuis on("input")
205
+
158
206
  this.executeQueryAndProcess = async (dbConnection, queryString, queryParams, msg) => {
159
207
  const result = await dbConnection.query(queryString, queryParams);
160
-
161
208
  if (typeof result === "undefined") { throw new Error("Query returned undefined."); }
162
209
  const newMsg = RED.util.cloneMessage(msg);
163
210
  const otherParams = {};
@@ -198,148 +245,132 @@ module.exports = function (RED) {
198
245
  if (Object.keys(otherParams).length) { newMsg.odbc = otherParams; }
199
246
  return newMsg;
200
247
  };
201
-
202
- // =================================================================
203
- // NOUVELLE IMPLEMENTATION DU STREAMING
204
- // =================================================================
248
+
205
249
  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);
250
+ const chunkSize = parseInt(this.config.streamChunkSize) || 1;
251
+ const fetchSize = chunkSize > 100 ? 100 : chunkSize;
252
+ let cursor;
253
+
254
+ try {
255
+ cursor = await dbConnection.query(queryString, queryParams, { cursor: true, fetchSize: fetchSize });
256
+ this.status({ fill: "blue", shape: "dot", text: "streaming rows..." });
257
+
258
+ let rowCount = 0;
259
+ let chunk = [];
260
+
261
+ while (true) {
262
+ const rows = await cursor.fetch();
263
+ if (!rows || rows.length === 0) {
264
+ break;
238
265
  }
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 = [];
266
+
267
+ for (const row of rows) {
268
+ rowCount++;
269
+ chunk.push(row);
270
+ if (chunk.length >= chunkSize) {
271
+ const newMsg = RED.util.cloneMessage(msg);
272
+ objPath.set(newMsg, this.config.outputObj, chunk);
273
+ newMsg.odbc_stream = { index: rowCount - chunk.length, count: chunk.length, complete: false };
274
+ send(newMsg);
275
+ chunk = [];
276
+ }
255
277
  }
256
278
  }
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();
279
+
280
+ if (chunk.length > 0) {
281
+ const newMsg = RED.util.cloneMessage(msg);
282
+ objPath.set(newMsg, this.config.outputObj, chunk);
283
+ newMsg.odbc_stream = { index: rowCount - chunk.length, count: chunk.length, complete: false };
284
+ send(newMsg);
285
+ }
286
+
287
+ const finalMsg = RED.util.cloneMessage(msg);
288
+ objPath.set(finalMsg, this.config.outputObj, []);
289
+ finalMsg.odbc_stream = { index: rowCount, count: 0, complete: true };
290
+ send(finalMsg);
291
+
292
+ this.status({ fill: "green", shape: "dot", text: `success (${rowCount} rows)` });
293
+
294
+ } finally {
295
+ if (cursor) {
296
+ await cursor.close();
297
+ }
264
298
  }
265
- }
266
- };
299
+ };
267
300
 
268
- // =================================================================
269
- // NOUVELLE LOGIQUE D'ENTREE UNIFIEE
270
- // =================================================================
271
301
  this.on("input", async (msg, send, done) => {
272
302
  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;
303
+ return done(new Error("ODBC Config node not properly configured."));
277
304
  }
278
-
279
- let connection;
280
- try {
281
- this.status({ fill: "blue", shape: "dot", text: "preparing..." });
305
+
306
+ const execute = async (connection) => {
282
307
  this.config.outputObj = this.config.outputObj || "payload";
283
-
284
- // Obtenir la requête et les paramètres
308
+
285
309
  const querySourceType = this.config.querySourceType || 'msg';
286
310
  const querySource = this.config.querySource || 'query';
287
311
  const paramsSourceType = this.config.paramsSourceType || 'msg';
288
312
  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
313
 
298
- if (!currentQueryString) { throw new Error("No query to execute"); }
314
+ const params = await new Promise(resolve => RED.util.evaluateNodeProperty(paramsSource, paramsSourceType, this, msg, (err, val) => resolve(err ? undefined : val)));
315
+ let query = await new Promise(resolve => RED.util.evaluateNodeProperty(querySource, querySourceType, this, msg, (err, val) => resolve(err ? undefined : (val || this.config.query || ""))));
299
316
 
300
- const isPreparedStatement = currentQueryParams || (currentQueryString && currentQueryString.includes("?"));
301
- if (!isPreparedStatement && currentQueryString) {
302
- currentQueryString = mustache.render(currentQueryString, msg);
317
+ if (!query) throw new Error("No query to execute");
318
+
319
+ const isPreparedStatement = params || (query && query.includes("?"));
320
+ if (!isPreparedStatement && query) {
321
+ query = mustache.render(query, msg);
303
322
  }
304
323
 
305
- // Obtenir une connexion du pool
306
- this.status({ fill: "yellow", shape: "dot", text: "connecting..." });
307
- connection = await this.poolNode.connect();
308
324
  this.status({ fill: "blue", shape: "dot", text: "executing..." });
309
-
310
325
  if (this.config.streaming) {
311
- await this.executeStreamQuery(connection, currentQueryString, currentQueryParams, msg, send);
326
+ await this.executeStreamQuery(connection, query, params, msg, send);
312
327
  } else {
313
- const newMsg = await this.executeQueryAndProcess(connection, currentQueryString, currentQueryParams, msg);
328
+ const newMsg = await this.executeQueryAndProcess(connection, query, params, msg);
314
329
  this.status({ fill: "green", shape: "dot", text: "success" });
315
330
  send(newMsg);
316
331
  }
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
325
-
326
- } finally {
327
- if (connection) {
332
+ };
333
+
334
+ let connectionFromPool;
335
+ try {
336
+ this.status({ fill: "yellow", shape: "dot", text: "connecting..." });
337
+ connectionFromPool = await this.poolNode.connect();
338
+ await execute(connectionFromPool);
339
+ return done();
340
+ } catch (poolError) {
341
+ this.warn(`First attempt with pooled connection failed: ${poolError.message}`);
342
+ if (this.poolNode.config.retryFreshConnection) {
343
+ this.warn("Attempting retry with a fresh connection.");
344
+ this.status({ fill: "yellow", shape: "dot", text: "Retrying (fresh)..." });
345
+ let freshConnection;
328
346
  try {
329
- await connection.close();
330
- } catch (closeErr) {
331
- this.warn(`Failed to close DB connection: ${closeErr.message}`);
347
+ const freshConnectConfig = this.poolNode.getFreshConnectionConfig();
348
+ freshConnection = await odbcModule.connect(freshConnectConfig);
349
+ this.log("Fresh connection established for retry.");
350
+ await execute(freshConnection);
351
+ this.log("Query successful with fresh connection. Resetting pool.");
352
+ await this.poolNode.resetPool();
353
+ return done();
354
+ } catch (freshError) {
355
+ this.status({ fill: "red", shape: "ring", text: "retry failed" });
356
+ return done(this.enhanceError(freshError, null, null, "Retry with fresh connection also failed"));
357
+ } finally {
358
+ if (freshConnection) await freshConnection.close();
332
359
  }
360
+ } else {
361
+ this.status({ fill: "red", shape: "ring", text: "query error" });
362
+ return done(this.enhanceError(poolError));
333
363
  }
364
+ } finally {
365
+ if (connectionFromPool) await connectionFromPool.close();
334
366
  }
335
367
  });
336
368
 
337
369
  this.on("close", (done) => {
338
370
  this.status({});
339
- // La logique de fermeture du pool est déjà dans le noeud de config
340
371
  done();
341
372
  });
342
-
373
+
343
374
  if (this.poolNode) {
344
375
  this.status({ fill: "green", shape: "dot", text: "ready" });
345
376
  } else {
@@ -347,6 +378,6 @@ module.exports = function (RED) {
347
378
  this.warn("ODBC Config node not found or not deployed.");
348
379
  }
349
380
  }
350
-
381
+
351
382
  RED.nodes.registerType("odbc", odbc);
352
383
  };
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.1",
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",