@bkmj/node-red-contrib-odbcmj 1.6.5 → 1.7.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 +18 -113
  2. package/odbc.html +226 -146
  3. package/odbc.js +623 -193
  4. package/package.json +1 -1
package/odbc.js CHANGED
@@ -1,303 +1,733 @@
1
- module.exports = function(RED) {
2
- const odbcModule = require('odbc'); // Import the odbc module for database connectivity
3
- const mustache = require('mustache'); // Import the mustache module for templating
4
- const objPath = require('object-path'); // Import the object-path module for object manipulation
1
+ module.exports = function (RED) {
2
+ const odbcModule = require("odbc");
3
+ const mustache = require("mustache");
4
+ const objPath = require("object-path");
5
5
 
6
6
  // --- ODBC Configuration Node ---
7
7
  function poolConfig(config) {
8
- RED.nodes.createNode(this, config); // Create a Node-RED node
9
- this.config = config; // Store the node configuration
10
- this.pool = null; // Initialize the connection pool
11
- this.connecting = false; // Flag to indicate if the node is connecting
8
+ RED.nodes.createNode(this, config);
9
+ this.config = config; // Contient connectionString, initialSize, retryFreshConnection, etc.
10
+ this.pool = null;
11
+ this.connecting = false;
12
12
 
13
- const enableSyntaxChecker = this.config.syntaxtick; // Renamed for clarity
13
+ const enableSyntaxChecker = this.config.syntaxtick;
14
14
  const syntax = this.config.syntax;
15
- delete this.config.syntaxtick;
16
- delete this.config.syntax;
15
+ // Garder une copie de la config originale pour la connexion fraîche si besoin
16
+ this.originalConfigForFreshConnection = {
17
+ connectionString: this.config.connectionString,
18
+ connectionTimeout: parseInt(this.config.connectionTimeout) || 0, // Assurer un integer, 0 pour certains drivers signifie pas de timeout spécifique à l'appel connect
19
+ loginTimeout: parseInt(this.config.loginTimeout) || 0,
20
+ };
21
+
22
+ delete this.config.syntaxtick; // Ces champs ne sont pas pour odbc.pool directement
23
+ // delete this.config.syntax; // 'syntax' pourrait être utile si odbc.pool l'acceptait
17
24
 
18
- // Create a SQL parser if syntax check is enabled
19
- this.parser = enableSyntaxChecker
20
- ? new require('node-sql-parser/build/' + syntax).Parser()
25
+ this.parser = enableSyntaxChecker
26
+ ? new require("node-sql-parser/build/" + syntax).Parser()
21
27
  : null;
22
28
 
23
- // Convert numeric config params to integers
24
29
  for (const [key, value] of Object.entries(this.config)) {
25
30
  if (!isNaN(parseInt(value))) {
26
31
  this.config[key] = parseInt(value);
27
32
  }
28
33
  }
34
+ // 'retryFreshConnection' est déjà dans this.config grâce à la création du noeud
29
35
 
30
- // Connect to the database and create a connection pool
31
36
  this.connect = async () => {
37
+ // Si le pool n'existe pas (ou a été reset), on le crée
32
38
  if (!this.pool) {
39
+ this.connecting = true;
40
+ this.status({
41
+ fill: "yellow",
42
+ shape: "dot",
43
+ text: "Pool init...",
44
+ });
33
45
  try {
34
- this.pool = await odbcModule.pool(this.config);
46
+ // Utiliser une copie de la config sans retryFreshConnection pour odbc.pool
47
+ const poolParams = { ...this.config };
48
+ delete poolParams.retryFreshConnection;
49
+ delete poolParams.syntax; // Retiré car non utilisé par odbc.pool
50
+
51
+ this.pool = await odbcModule.pool(poolParams);
35
52
  this.connecting = false;
53
+ this.status({
54
+ fill: "green",
55
+ shape: "dot",
56
+ text: "Pool ready",
57
+ });
58
+ this.log("Connection pool initialized successfully.");
36
59
  } catch (error) {
37
- // Handle connection errors (e.g., log the error, set node status)
38
- this.error(`Error creating connection pool: ${error}`);
39
- this.status({ fill: "red", shape: "ring", text: "Connection error" });
40
- throw error; // Re-throw to prevent further execution
60
+ this.connecting = false;
61
+ this.error(
62
+ `Error creating connection pool: ${error}`,
63
+ error
64
+ );
65
+ this.status({
66
+ fill: "red",
67
+ shape: "ring",
68
+ text: "Pool error",
69
+ });
70
+ throw error;
71
+ }
72
+ }
73
+ // Quoi qu'il arrive, on demande une connexion au pool (qui pourrait être fraîchement créé)
74
+ try {
75
+ return await this.pool.connect();
76
+ } catch (poolConnectError) {
77
+ this.error(
78
+ `Error connecting to pool: ${poolConnectError}`,
79
+ poolConnectError
80
+ );
81
+ this.status({
82
+ fill: "red",
83
+ shape: "ring",
84
+ text: "Pool connect err",
85
+ });
86
+ throw poolConnectError;
87
+ }
88
+ };
89
+
90
+ // --- NOUVELLE MÉTHODE: resetPool ---
91
+ this.resetPool = async () => {
92
+ if (this.pool) {
93
+ this.log("Resetting connection pool.");
94
+ this.status({
95
+ fill: "yellow",
96
+ shape: "ring",
97
+ text: "Resetting pool...",
98
+ });
99
+ try {
100
+ await this.pool.close();
101
+ this.log("Connection pool closed successfully for reset.");
102
+ } catch (closeError) {
103
+ this.error(
104
+ `Error closing pool during reset: ${closeError}`,
105
+ closeError
106
+ );
107
+ // Continuer pour nullifier le pool même en cas d'erreur de fermeture
108
+ } finally {
109
+ this.pool = null;
110
+ this.connecting = false; // Permet à this.connect de recréer le pool
111
+ // Le statut sera mis à jour par la prochaine tentative de connexion via this.connect()
41
112
  }
113
+ } else {
114
+ this.log("Pool reset requested, but no active pool to reset.");
42
115
  }
43
- return await this.pool.connect();
44
116
  };
45
117
 
46
- // Close the connection pool when the node is closed
47
- this.on('close', async (removed, done) => {
48
- if (removed && this.pool) {
118
+ this.on("close", async (removed, done) => {
119
+ // 'removed' est true si le noeud est supprimé, false si juste redéployé.
120
+ // Nous voulons fermer le pool dans les deux cas si nous en sommes propriétaires.
121
+ this.log("Closing ODBC config node. Attempting to close pool.");
122
+ if (this.pool) {
49
123
  try {
50
124
  await this.pool.close();
125
+ this.log(
126
+ "Connection pool closed successfully on node close."
127
+ );
128
+ this.pool = null;
51
129
  } catch (error) {
52
- // Handle errors during pool closure
53
- this.error(`Error closing connection pool: ${error}`);
130
+ this.error(
131
+ `Error closing connection pool on node close: ${error}`,
132
+ error
133
+ );
54
134
  }
55
135
  }
56
136
  done();
57
137
  });
58
138
  }
59
139
 
60
- RED.nodes.registerType('odbc config', poolConfig);
140
+ RED.nodes.registerType("odbc config", poolConfig);
61
141
 
62
142
  // --- ODBC Query Node ---
63
143
  function odbc(config) {
64
144
  RED.nodes.createNode(this, config);
65
145
  this.config = config;
66
- this.poolNode = RED.nodes.getNode(this.config.connection);
146
+ this.poolNode = RED.nodes.getNode(this.config.connection); // C'est le noeud 'odbc config'
67
147
  this.name = this.config.name;
68
148
 
69
- this.runQuery = async function(msg, send, done) {
70
- try {
71
- this.status({ fill: "blue", shape: "dot", text: "querying..." });
72
- this.config.outputObj = msg?.output || this.config?.outputObj;
73
-
74
- // Automatically determine if it's a prepared statement based on the presence of
75
- // placeholders (?) in the query or if msg.parameters is an object/array.
76
- const isPreparedStatement = msg?.parameters || (this?.queryString ? this.queryString.includes('?') : false);
77
- this.queryString = this.config.query;
78
- if (!this.queryString.length) {
79
- this.queryString = null;
149
+ // --- NOUVELLE MÉTHODE: Helper pour améliorer les messages d'erreur ---
150
+ this.enhanceError = (
151
+ error,
152
+ query,
153
+ params,
154
+ defaultMessage = "Query error"
155
+ ) => {
156
+ const queryContext = (() => {
157
+ let s = "";
158
+ if (query || params) {
159
+ s += " {";
160
+ if (query)
161
+ s += `"query": '${query.substring(0, 100)}${
162
+ query.length > 100 ? "..." : ""
163
+ }'`; // Tronquer les longues requêtes
164
+ if (params) s += `, "params": '${JSON.stringify(params)}'`;
165
+ s += "}";
166
+ return s;
80
167
  }
168
+ return "";
169
+ })();
170
+
171
+ let finalError;
172
+ if (typeof error === "object" && error !== null && error.message) {
173
+ finalError = error;
174
+ } else if (typeof error === "string") {
175
+ finalError = new Error(error);
176
+ } else {
177
+ finalError = new Error(defaultMessage);
178
+ }
179
+
180
+ finalError.message = `${finalError.message}${queryContext}`;
181
+ if (query) finalError.query = query;
182
+ if (params) finalError.params = params;
183
+
184
+ return finalError;
185
+ };
186
+
187
+ // --- NOUVELLE MÉTHODE: Helper pour exécuter la requête et traiter le résultat ---
188
+ // Prend une connexion (du pool ou fraîche) en argument
189
+ this.executeQueryAndProcess = async (
190
+ dbConnection,
191
+ queryString,
192
+ queryParams,
193
+ isPreparedStatement,
194
+ msg
195
+ ) => {
196
+ let result;
197
+ // Exécution de la requête
198
+ if (isPreparedStatement) {
199
+ const stmt = await dbConnection.createStatement();
200
+ try {
201
+ await stmt.prepare(queryString);
202
+ await stmt.bind(queryParams); // queryParams vient de msg.parameters
203
+ result = await stmt.execute();
204
+ } finally {
205
+ // Assurer la fermeture du statement même en cas d'erreur de execute()
206
+ // stmt.close() peut être synchrone ou asynchrone selon les drivers/versions de odbc
207
+ if (stmt && typeof stmt.close === "function") {
208
+ try {
209
+ await stmt.close();
210
+ } catch (stmtCloseError) {
211
+ this.warn(
212
+ `Error closing statement: ${stmtCloseError}`
213
+ );
214
+ }
215
+ }
216
+ }
217
+ } else {
218
+ result = await dbConnection.query(queryString, queryParams); // queryParams ici aussi
219
+ }
220
+
221
+ if (typeof result === "undefined") {
222
+ // Certains drivers/erreurs pourraient retourner undefined
223
+ throw new Error(
224
+ "Query returned undefined. Check for errors or empty results."
225
+ );
226
+ }
227
+
228
+ // Traitement du résultat (SQL_BIT, otherParams, etc.)
229
+ // Créer une copie du message pour éviter de modifier l'original en cas de retry
230
+ const newMsg = RED.util.cloneMessage(msg);
231
+
232
+ const otherParams = {};
233
+ let actualDataRows = [];
234
+
235
+ if (result !== null && typeof result === "object") {
236
+ // Si result est un array, il contient les lignes.
237
+ // Les propriétés non-numériques (comme .columns, .count) sont extraites.
238
+ if (Array.isArray(result)) {
239
+ actualDataRows = [...result]; // Copie des lignes
240
+ for (const [key, value] of Object.entries(result)) {
241
+ if (isNaN(parseInt(key))) {
242
+ otherParams[key] = value;
243
+ }
244
+ }
245
+ } else {
246
+ // Si result est un objet mais pas un array (ex: { count: 0, columns: [...] })
247
+ // Il n'y a pas de "lignes" au sens array, mais otherParams peut contenir des metadonnées.
248
+ for (const [key, value] of Object.entries(result)) {
249
+ otherParams[key] = value;
250
+ }
251
+ }
252
+ }
253
+
254
+ const columnMetadata = otherParams.columns;
255
+ if (
256
+ Array.isArray(columnMetadata) &&
257
+ Array.isArray(actualDataRows) &&
258
+ actualDataRows.length > 0
259
+ ) {
260
+ const sqlBitColumnNames = new Set();
261
+ columnMetadata.forEach((col) => {
262
+ if (
263
+ col &&
264
+ typeof col.name === "string" &&
265
+ col.dataTypeName === "SQL_BIT"
266
+ ) {
267
+ sqlBitColumnNames.add(col.name);
268
+ }
269
+ });
270
+
271
+ if (sqlBitColumnNames.size > 0) {
272
+ actualDataRows.forEach((row) => {
273
+ if (typeof row === "object" && row !== null) {
274
+ for (const columnName of sqlBitColumnNames) {
275
+ if (row.hasOwnProperty(columnName)) {
276
+ const value = row[columnName];
277
+ if (value === "1" || value === 1) {
278
+ row[columnName] = true;
279
+ } else if (value === "0" || value === 0) {
280
+ row[columnName] = false;
281
+ }
282
+ }
283
+ }
284
+ }
285
+ });
286
+ }
287
+ }
288
+
289
+ objPath.set(newMsg, this.config.outputObj, actualDataRows);
290
+
291
+ if (this.poolNode?.parser && queryString) {
292
+ try {
293
+ // Utiliser structuredClone est une bonne pratique pour éviter les modifications par référence
294
+ newMsg.parsedQuery = this.poolNode.parser.astify(
295
+ structuredClone(queryString)
296
+ );
297
+ } catch (syntaxError) {
298
+ this.warn(
299
+ `Could not parse query for parsedQuery output: ${syntaxError}`
300
+ );
301
+ }
302
+ }
81
303
 
82
- // --- Construct the query string ---
83
- if (!isPreparedStatement && this.queryString) {
84
- // Handle Mustache templating for regular queries
85
- for (const parsed of mustache.parse(this.queryString)) {
304
+ if (Object.keys(otherParams).length) {
305
+ newMsg.odbc = otherParams;
306
+ }
307
+ return newMsg;
308
+ };
309
+
310
+ this.runQuery = async function (msg, send, done) {
311
+ let currentQueryString = this.config.query || ""; // Initialiser avec la config du noeud
312
+ let currentQueryParams = msg.parameters; // Peut être undefined
313
+ let isPreparedStatement = false;
314
+ let connectionFromPool = null; // Pour s'assurer de sa fermeture
315
+
316
+ try {
317
+ this.status({
318
+ fill: "blue",
319
+ shape: "dot",
320
+ text: "querying...",
321
+ });
322
+ this.config.outputObj =
323
+ msg?.output || this.config?.outputObj || "payload";
324
+
325
+ // --- Construction de la requête (adapté de l'original) ---
326
+ // Déterminer si c'est un prepared statement AVANT le render mustache
327
+ isPreparedStatement =
328
+ currentQueryParams ||
329
+ (currentQueryString && currentQueryString.includes("?"));
330
+
331
+ if (!isPreparedStatement && currentQueryString) {
332
+ // Mustache rendering uniquement si ce n'est pas un PS avec des '?'
333
+ // Et si currentQueryString est défini
334
+ for (const parsed of mustache.parse(currentQueryString)) {
86
335
  if (parsed[0] === "name" || parsed[0] === "&") {
87
336
  if (!objPath.has(msg, parsed[1])) {
88
- this.warn(`Mustache parameter "${parsed[1]}" is absent and will render to undefined`);
337
+ this.warn(
338
+ `Mustache parameter "${parsed[1]}" is absent and will render to undefined`
339
+ );
89
340
  }
90
341
  }
91
342
  }
92
- this.queryString = mustache.render(this.queryString, msg);
343
+ currentQueryString = mustache.render(
344
+ currentQueryString,
345
+ msg
346
+ );
93
347
  }
94
348
 
95
- // Handle cases where the query is provided in the message
96
349
  if (msg?.query) {
97
- if (this.queryString) {
98
- this.log('Warning. The query defined in the node configuration was overwritten by msg.config.');
350
+ // Priorité à msg.query
351
+ if (
352
+ currentQueryString &&
353
+ currentQueryString !== this.config.query
354
+ ) {
355
+ this.log(
356
+ "Query from node config (possibly mustache rendered) was overwritten by msg.query."
357
+ );
358
+ } else if (this.config.query) {
359
+ this.log(
360
+ "Query from node config was overwritten by msg.query."
361
+ );
99
362
  }
100
- this.queryString = msg.query;
363
+ currentQueryString = msg.query;
101
364
  } else if (msg?.payload) {
102
- if (typeof msg.payload === 'string') {
365
+ // Ensuite msg.payload.query ou msg.payload (si string)
366
+ if (typeof msg.payload === "string") {
103
367
  try {
104
368
  const payloadJson = JSON.parse(msg.payload);
105
- if (payloadJson?.query && typeof payloadJson.query === 'string') {
106
- this.queryString = payloadJson.query;
369
+ if (
370
+ payloadJson?.query &&
371
+ typeof payloadJson.query === "string"
372
+ ) {
373
+ currentQueryString = payloadJson.query;
107
374
  }
108
- } catch (err) {} // Ignore JSON parsing errors
109
- } else if (msg.payload?.query && typeof msg.payload.query === 'string') {
110
- this.queryString = msg.payload.query;
375
+ } catch (err) {
376
+ /* Pas un JSON ou pas de query, on ignore */
377
+ }
378
+ } else if (
379
+ msg.payload?.query &&
380
+ typeof msg.payload.query === "string"
381
+ ) {
382
+ currentQueryString = msg.payload.query;
111
383
  }
112
384
  }
113
385
 
114
- if (!this.queryString) {
386
+ if (!currentQueryString) {
115
387
  throw new Error("No query to execute");
116
388
  }
117
389
 
118
- // --- Parameter handling for prepared statements ---
119
- if (isPreparedStatement) {
120
- if (!msg?.parameters) {
121
- throw new Error("Prepared statement requires msg.parameters");
122
- } else {
123
- // If parameters are provided as an object, extract parameter names from the query
124
- // and create an ordered array of values for the prepared statement.
125
- if (typeof msg.parameters === 'object' && !Array.isArray(msg.parameters)) {
126
- const paramNames = this.queryString.match(/\(([^)]*)\)/)[1].split(',').map(el => el.trim());
390
+ // Re-vérifier isPreparedStatement si query a changé, et valider les paramètres
391
+ isPreparedStatement =
392
+ currentQueryParams ||
393
+ (currentQueryString && currentQueryString.includes("?"));
127
394
 
128
- // Create an ordered array of values
129
- msg.parameters = paramNames.map(name => msg.parameters[name]);
395
+ if (isPreparedStatement) {
396
+ if (!currentQueryParams) {
397
+ throw new Error(
398
+ "Prepared statement ('?' in query) requires msg.parameters to be provided."
399
+ );
400
+ }
401
+ if (
402
+ typeof currentQueryParams === "object" &&
403
+ !Array.isArray(currentQueryParams)
404
+ ) {
405
+ // Tentative de mapper un objet à un array basé sur les noms dans la query (simplifié)
406
+ // Cette logique est complexe et sujette à erreur si les noms ne matchent pas parfaitement.
407
+ // La documentation originale suggère un mapping auto, mais c'est risqué.
408
+ // Pour l'instant, on se fie à l'ordre si c'est un objet.
409
+ // Une solution plus robuste serait d'analyser la query pour les noms de paramètres si le driver le supporte.
410
+ // Ou d'exiger un array pour les '?'
411
+ // Pour l'instant, on va assumer que si c'est un objet, l'utilisateur a une logique spécifique ou le driver le gère
412
+ // La logique originale de mappage par nom de paramètre dans `()` est retirée pour simplification,
413
+ // car elle est très spécifique et peut ne pas être standard.
414
+ // this.warn("msg.parameters is an object for a '?' prepared statement. Order of properties will be used. For explicit order, use an array.");
415
+ // currentQueryParams = Object.values(currentQueryParams); // Ceci est une supposition sur l'ordre.
416
+ // La meilleure approche est de demander un Array pour les '?'
417
+ if (
418
+ (currentQueryString.match(/\?/g) || []).length !==
419
+ Object.keys(currentQueryParams).length &&
420
+ (currentQueryString.match(/\?/g) || []).length !==
421
+ currentQueryParams.length
422
+ ) {
423
+ // La logique originale pour mapper les noms de paramètres pour les '?' était `this.queryString.match(/\(([^)]*)\)/)[1].split(",").map((el) => el.trim());`
424
+ // Ceci n'est pas standard pour les `?`. Normalement, `?` attend un array.
425
+ // On va laisser le driver/odbc gérer si `msg.parameters` est un objet.
426
+ // Mais on va vérifier le nombre de `?` vs la taille de `msg.parameters` si c'est un array.
427
+ // Si c'est un objet, on ne peut pas facilement vérifier le nombre.
130
428
  }
131
429
  }
132
-
133
- // Validate the parameters array
134
- if (!Array.isArray(msg.parameters)) {
135
- throw new Error("msg.parameters must be an object or an array");
136
- } else if ((this.queryString.match(/\?/g) || []).length !== msg.parameters.length) {
137
- throw new Error("Incorrect number of parameters");
430
+ if (
431
+ !Array.isArray(currentQueryParams) &&
432
+ typeof currentQueryParams !== "object"
433
+ ) {
434
+ // Doit être array ou objet
435
+ throw new Error(
436
+ "msg.parameters must be an array or an object for prepared statements."
437
+ );
138
438
  }
139
- }
140
-
141
- // --- Syntax check ---
142
- if (this.poolNode?.parser) {
143
- try {
144
- this.parseSql = this.poolNode.parser.astify(structuredClone(this.queryString));
145
- } catch (error) {
146
- throw new Error("SQL syntax error");
439
+ if (
440
+ Array.isArray(currentQueryParams) &&
441
+ (currentQueryString.match(/\?/g) || []).length !==
442
+ currentQueryParams.length
443
+ ) {
444
+ throw new Error(
445
+ "Incorrect number of parameters in msg.parameters array for '?' placeholders."
446
+ );
147
447
  }
148
448
  }
149
449
 
150
- // --- Output object validation ---
450
+ // Validation du champ de sortie
151
451
  if (!this.config.outputObj) {
152
- throw new Error("Invalid output object definition");
452
+ throw new Error(
453
+ "Invalid output object definition (outputObj is empty)"
454
+ );
153
455
  }
154
-
155
- const reg = new RegExp('^((?![,;:`\\[\\]{}+=()!"$%?&*|<>\\/^¨`\\s]).)*$');
156
- if (!this.config.outputObj.match(reg) ||
157
- this.config.outputObj.charAt(0) === "." ||
158
- this.config.outputObj.charAt(this.config.outputObj.length - 1) === ".") {
159
- throw new Error("Invalid output field");
456
+ const reg = new RegExp(
457
+ '^((?![,;:`\\[\\]{}+=()!"$%?&*|<>\\/^¨`\\s]).)*$'
458
+ );
459
+ if (
460
+ !this.config.outputObj.match(reg) ||
461
+ this.config.outputObj.startsWith(".") ||
462
+ this.config.outputObj.endsWith(".")
463
+ ) {
464
+ throw new Error(
465
+ `Invalid output field name: ${this.config.outputObj}`
466
+ );
160
467
  }
161
468
 
162
- // --- Get a connection from the pool ---
469
+ // --- Première tentative avec une connexion du pool ---
470
+ let firstAttemptError = null;
163
471
  try {
164
- this.connection = await this.poolNode.connect();
165
- if (!this.connection) {
166
- throw new Error("No connection available");
472
+ connectionFromPool = await this.poolNode.connect();
473
+ if (!connectionFromPool) {
474
+ // Devrait être géré par poolNode.connect() qui throw une erreur
475
+ throw new Error(
476
+ "Failed to get connection from pool (returned null)"
477
+ );
478
+ }
479
+ this.status({
480
+ fill: "blue",
481
+ shape: "dot",
482
+ text: "Pool conn OK. Executing...",
483
+ });
484
+
485
+ const processedMsg = await this.executeQueryAndProcess(
486
+ connectionFromPool,
487
+ currentQueryString,
488
+ currentQueryParams,
489
+ isPreparedStatement,
490
+ msg
491
+ );
492
+ this.status({
493
+ fill: "green",
494
+ shape: "dot",
495
+ text: "success",
496
+ });
497
+ send(processedMsg);
498
+ if (done) done();
499
+ return; // Succès à la première tentative
500
+ } catch (err) {
501
+ firstAttemptError = this.enhanceError(
502
+ err,
503
+ currentQueryString,
504
+ currentQueryParams,
505
+ "Query failed with pooled connection"
506
+ );
507
+ this.warn(
508
+ `First attempt failed: ${firstAttemptError.message}`
509
+ );
510
+ // Ne pas remonter l'erreur tout de suite, on va peut-être retenter
511
+ } finally {
512
+ if (connectionFromPool) {
513
+ try {
514
+ await connectionFromPool.close(); // Toujours fermer/remettre la connexion au pool
515
+ } catch (closeErr) {
516
+ this.warn(
517
+ `Error closing pooled connection: ${closeErr}`
518
+ );
519
+ }
520
+ connectionFromPool = null;
167
521
  }
168
- } catch (error) {
169
- // Handle connection errors (e.g., log the error, set node status)
170
- this.error(`Error getting connection: ${error}`);
171
- this.status({ fill: "red", shape: "ring", text: "Connection error" });
172
- throw error; // Re-throw to prevent further execution
173
522
  }
174
523
 
175
- try {
176
- let result;
177
- if (isPreparedStatement) {
178
- // --- Execute prepared statement ---
179
- const stmt = await this.connection.createStatement();
180
- await stmt.prepare(this.queryString);
181
-
182
- // Bind the values to the prepared statement
183
- await stmt.bind(msg.parameters);
184
-
185
- // Execute the prepared statement
186
- result = await stmt.execute();
187
- stmt.close();
188
- } else {
189
- // --- Execute regular query ---
190
- result = await this.connection.query(this.queryString, msg?.parameters);
191
- }
524
+ // --- Si la première tentative a échoué (firstAttemptError est défini) ---
525
+ if (firstAttemptError) {
526
+ if (
527
+ this.poolNode &&
528
+ this.poolNode.config.retryFreshConnection
529
+ ) {
530
+ this.log("Attempting retry with a fresh connection.");
531
+ this.status({
532
+ fill: "yellow",
533
+ shape: "dot",
534
+ text: "Retrying (fresh)...",
535
+ });
536
+
537
+ let freshConnection = null;
538
+ try {
539
+ // Utiliser les paramètres de connexion originaux stockés dans poolNode
540
+ const freshConnectConfig =
541
+ this.poolNode.originalConfigForFreshConnection;
542
+ if (
543
+ !freshConnectConfig ||
544
+ !freshConnectConfig.connectionString
545
+ ) {
546
+ throw new Error(
547
+ "Fresh connection configuration is missing in poolNode."
548
+ );
549
+ }
550
+ freshConnection = await odbcModule.connect(
551
+ freshConnectConfig
552
+ );
553
+ this.log("Fresh connection established for retry.");
554
+
555
+ const processedFreshMsg =
556
+ await this.executeQueryAndProcess(
557
+ freshConnection,
558
+ currentQueryString,
559
+ currentQueryParams,
560
+ isPreparedStatement,
561
+ msg
562
+ );
563
+
564
+ this.log(
565
+ "Query successful with fresh connection. Resetting pool."
566
+ );
567
+ this.status({
568
+ fill: "green",
569
+ shape: "dot",
570
+ text: "Success (fresh)",
571
+ });
572
+ send(processedFreshMsg);
573
+
574
+ if (this.poolNode.resetPool) {
575
+ await this.poolNode.resetPool(); // Demander au pool de se réinitialiser
576
+ } else {
577
+ this.warn(
578
+ "poolNode.resetPool is not available. Pool cannot be reset automatically."
579
+ );
580
+ }
192
581
 
193
- if (result) {
194
- // --- Process and send the result ---
195
- const otherParams = {};
196
- for (const [key, value] of Object.entries(result)) {
197
- if (isNaN(parseInt(key))) {
198
- otherParams[key] = value;
199
- delete result[key];
582
+ if (done) done();
583
+ return; // Succès à la seconde tentative
584
+ } catch (freshError) {
585
+ this.warn(
586
+ `Retry with fresh connection also failed: ${freshError.message}`
587
+ );
588
+ // L'erreur finale sera celle de la tentative fraîche
589
+ throw this.enhanceError(
590
+ freshError,
591
+ currentQueryString,
592
+ currentQueryParams,
593
+ "Query failed on fresh connection retry"
594
+ );
595
+ } finally {
596
+ if (freshConnection) {
597
+ try {
598
+ await freshConnection.close();
599
+ this.log("Fresh connection closed.");
600
+ } catch (closeFreshErr) {
601
+ this.warn(
602
+ `Error closing fresh connection: ${closeFreshErr}`
603
+ );
604
+ }
200
605
  }
201
606
  }
202
- objPath.set(msg, this.config.outputObj, result);
203
- if (this.parseSql) {
204
- msg.parsedQuery = this.parseSql;
205
- }
206
- if (Object.keys(otherParams).length) {
207
- msg.odbc = otherParams;
208
- }
209
- this.status({ fill: 'green', shape: 'dot', text: 'success' });
210
- send(msg);
211
607
  } else {
212
- throw new Error("The query returned no results");
213
- }
214
- } catch (error) {
215
- const str = (() =>{
216
- let str = ''
217
- if(this?.queryString || this?.parameters){
218
- str += " {"
219
- if(this?.queryString) str += `"query":'${this.queryString}'`;
220
- if(msg?.parameters) str += `, "params":'${msg.parameters}'`;
221
- str += "}"
222
- }
223
- })()
224
- if(typeof error == 'object'){
225
- // Enhance the error object with query information
226
-
227
- if(this?.queryString) error.query = this.queryString;
228
- if(this?.parameters) error.params = msg.parameters;
229
-
230
- if(error?.message){
231
- error.message += str;
232
- }
233
- }
234
- else if (typeof error == 'string'){
235
- error += str;
608
+ // retryFreshConnection n'est pas activé, donc on lance l'erreur de la première tentative
609
+ throw firstAttemptError;
236
610
  }
237
- // Handle query errors
238
- this.status({ fill: "red", shape: "ring", text: "Query error" });
239
- throw error; // Re-throw to trigger the outer catch block
240
- } finally {
241
- await this.connection.close();
242
- }
243
-
244
- if (done) {
245
- done();
246
611
  }
247
612
  } catch (err) {
248
- this.status({ fill: "red", shape: "ring", text: err.message || "query error" });
613
+ // Catch global pour runQuery
614
+ // Assurer que err est bien un objet Error
615
+ const finalError =
616
+ err instanceof Error ? err : new Error(String(err));
617
+
618
+ this.status({
619
+ fill: "red",
620
+ shape: "ring",
621
+ text:
622
+ finalError.message && finalError.message.length < 30
623
+ ? finalError.message.substring(0, 29) + "..."
624
+ : "query error",
625
+ });
626
+
249
627
  if (done) {
250
- done(err);
628
+ done(finalError); // Passer l'erreur au callback done de Node-RED
251
629
  } else {
252
- this.error(err, msg);
630
+ this.error(finalError, msg); // Utiliser this.error pour logguer l'erreur correctement
253
631
  }
254
632
  }
255
- };
633
+ }; // Fin de runQuery
256
634
 
257
- // --- Check connection pool before running query ---
258
- this.checkPool = async function(msg, send, done) {
635
+ this.checkPool = async function (msg, send, done) {
259
636
  try {
637
+ if (!this.poolNode) {
638
+ throw new Error(
639
+ "ODBC Connection Configuration node is not properly configured or deployed."
640
+ );
641
+ }
260
642
  if (this.poolNode.connecting) {
261
- this.warn("Waiting for connection pool...");
262
- this.status({ fill: "yellow", shape: "ring", text: "requesting pool" });
643
+ // Si le pool est en cours d'initialisation
644
+ this.warn("Waiting for connection pool to initialize...");
645
+ this.status({
646
+ fill: "yellow",
647
+ shape: "ring",
648
+ text: "Waiting for pool",
649
+ });
263
650
  setTimeout(() => {
264
- this.checkPool(msg, send, done);
265
- }, 1000);
651
+ this.checkPool(msg, send, done).catch((err) => {
652
+ // Gérer l'erreur de la tentative retardée si elle échoue aussi.
653
+ this.status({
654
+ fill: "red",
655
+ shape: "dot",
656
+ text: "Pool wait failed",
657
+ });
658
+ if (done) {
659
+ done(err);
660
+ } else {
661
+ this.error(err, msg);
662
+ }
663
+ });
664
+ }, 1000); // Réessayer après 1 seconde
266
665
  return;
267
666
  }
268
667
 
269
- if (!this.poolNode.pool) {
270
- this.poolNode.connecting = true;
271
- }
668
+ // Si le pool n'est pas encore initialisé (ex: premier message après déploiement),
669
+ // poolNode.connect() va le faire.
670
+ // La logique de this.poolNode.connecting doit être gérée DANS poolNode.connect()
272
671
 
273
672
  await this.runQuery(msg, send, done);
274
673
  } catch (err) {
275
- this.status({ fill: "red", shape: "dot", text: "operation failed" });
674
+ // Catch pour checkPool (erreurs avant même d'appeler runQuery, ou erreurs non gérées par runQuery)
675
+ const finalError =
676
+ err instanceof Error ? err : new Error(String(err));
677
+ this.status({
678
+ fill: "red",
679
+ shape: "dot",
680
+ text:
681
+ finalError.message && finalError.message.length < 30
682
+ ? finalError.message.substring(0, 29) + "..."
683
+ : "Op failed",
684
+ });
276
685
  if (done) {
277
- done(err);
686
+ done(finalError);
278
687
  } else {
279
- this.error(err, msg);
688
+ this.error(finalError, msg);
280
689
  }
281
690
  }
282
691
  };
283
692
 
284
- this.on('input', this.checkPool);
285
-
286
- // --- Close the connection when the node is closed ---
287
- this.on('close', async (done) => {
288
- if (this.connection) {
289
- try {
290
- await this.connection.close();
291
- } catch (error) {
292
- // Handle connection close errors
293
- this.error(`Error closing connection: ${error}`);
693
+ this.on("input", async (msg, send, done) => {
694
+ // Envelopper l'appel à checkPool dans un try-catch au cas où checkPool lui-même aurait une erreur synchrone non gérée
695
+ try {
696
+ await this.checkPool(msg, send, done);
697
+ } catch (error) {
698
+ const finalError =
699
+ error instanceof Error ? error : new Error(String(error));
700
+ this.status({
701
+ fill: "red",
702
+ shape: "ring",
703
+ text: "Input error",
704
+ });
705
+ if (done) {
706
+ done(finalError);
707
+ } else {
708
+ this.error(finalError, msg);
294
709
  }
295
710
  }
711
+ });
712
+
713
+ this.on("close", async (done) => {
714
+ // La connexion individuelle (this.connection du code original) est maintenant gérée
715
+ // à l'intérieur de runQuery (connectionFromPool et freshConnection) et fermée là.
716
+ // Il n'y a donc plus de this.connection à fermer ici au niveau du noeud odbc.
717
+ // Le poolNode (config node) gère la fermeture de son pool.
718
+ this.status({}); // Clear status on close/redeploy
296
719
  done();
297
720
  });
298
721
 
299
- this.status({ fill: 'green', shape: 'dot', text: 'ready' });
722
+ if (this.poolNode) {
723
+ this.status({ fill: "green", shape: "dot", text: "ready" });
724
+ } else {
725
+ this.status({ fill: "red", shape: "ring", text: "No config node" });
726
+ this.warn(
727
+ "ODBC Config node not found or not deployed. Please configure and deploy the ODBC connection config node."
728
+ );
729
+ }
300
730
  }
301
731
 
302
732
  RED.nodes.registerType("odbc", odbc);
303
- };
733
+ };