@bkmj/node-red-contrib-odbcmj 1.7.0 → 2.0.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 +79 -28
  2. package/odbc.html +261 -260
  3. package/odbc.js +259 -536
  4. package/package.json +26 -7
package/odbc.js CHANGED
@@ -1,114 +1,98 @@
1
1
  module.exports = function (RED) {
2
2
  const odbcModule = require("odbc");
3
- const mustache = require("mustache");
4
- const objPath = require("object-path");
3
+ const mustache = require("mustache"); // Utilisé dans runQuery
4
+ const objPath = require("object-path"); // Utilisé pour mustache et le positionnement du résultat
5
5
 
6
6
  // --- ODBC Configuration Node ---
7
7
  function poolConfig(config) {
8
8
  RED.nodes.createNode(this, config);
9
- this.config = config; // Contient connectionString, initialSize, retryFreshConnection, etc.
9
+ this.config = config;
10
10
  this.pool = null;
11
11
  this.connecting = false;
12
+
13
+ this.credentials = RED.nodes.getCredentials(this.id);
12
14
 
13
- const enableSyntaxChecker = this.config.syntaxtick;
14
- const syntax = 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
24
-
25
- this.parser = enableSyntaxChecker
26
- ? new require("node-sql-parser/build/" + syntax).Parser()
27
- : null;
28
-
29
- for (const [key, value] of Object.entries(this.config)) {
30
- if (!isNaN(parseInt(value))) {
31
- this.config[key] = parseInt(value);
15
+ this._buildConnectionString = function() {
16
+ if (this.config.connectionMode === 'structured') {
17
+ if (!this.config.dbType || !this.config.server) {
18
+ throw new Error("En mode structuré, le type de base de données et le serveur sont requis.");
19
+ }
20
+ let driver;
21
+ let parts = [];
22
+ switch (this.config.dbType) {
23
+ case 'sqlserver': driver = 'ODBC Driver 17 for SQL Server'; break;
24
+ case 'postgresql': driver = 'PostgreSQL Unicode'; break;
25
+ case 'mysql': driver = 'MySQL ODBC 8.0 Unicode Driver'; break;
26
+ default: driver = this.config.driver || ''; break;
27
+ }
28
+ if(driver) parts.unshift(`DRIVER={${driver}}`);
29
+ parts.push(`SERVER=${this.config.server}`);
30
+ if (this.config.database) parts.push(`DATABASE=${this.config.database}`);
31
+ if (this.config.user) parts.push(`UID=${this.config.user}`);
32
+ if (this.credentials && this.credentials.password) parts.push(`PWD=${this.credentials.password}`);
33
+ return parts.join(';');
34
+ } else {
35
+ let connStr = this.config.connectionString || "";
36
+ if (this.credentials && this.credentials.password && connStr.includes('{{{password}}}')) {
37
+ connStr = connStr.replace('{{{password}}}', this.credentials.password);
38
+ }
39
+ return connStr;
32
40
  }
33
- }
34
- // 'retryFreshConnection' est déjà dans this.config grâce à la création du noeud
41
+ };
35
42
 
36
43
  this.connect = async () => {
37
- // Si le pool n'existe pas (ou a été reset), on le crée
38
44
  if (!this.pool) {
39
45
  this.connecting = true;
40
- this.status({
41
- fill: "yellow",
42
- shape: "dot",
43
- text: "Pool init...",
44
- });
46
+ this.status({ fill: "yellow", shape: "dot", text: "Pool init..." });
45
47
  try {
46
- // Utiliser une copie de la config sans retryFreshConnection pour odbc.pool
48
+ const finalConnectionString = this._buildConnectionString();
49
+ if (!finalConnectionString) throw new Error("La chaîne de connexion est vide.");
50
+
47
51
  const poolParams = { ...this.config };
48
- delete poolParams.retryFreshConnection;
49
- delete poolParams.syntax; // Retiré car non utilisé par odbc.pool
52
+ poolParams.connectionString = finalConnectionString;
50
53
 
54
+ ['retryFreshConnection', 'retryDelay', 'retryOnMsg', 'syntax', 'connectionMode', 'dbType', 'server', 'database', 'user', 'driver'].forEach(k => delete poolParams[k]);
55
+
51
56
  this.pool = await odbcModule.pool(poolParams);
52
57
  this.connecting = false;
53
- this.status({
54
- fill: "green",
55
- shape: "dot",
56
- text: "Pool ready",
57
- });
58
+ this.status({ fill: "green", shape: "dot", text: "Pool ready" });
58
59
  this.log("Connection pool initialized successfully.");
59
60
  } catch (error) {
60
61
  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
- });
62
+ this.error(`Error creating connection pool: ${error.message}`, error);
63
+ this.status({ fill: "red", shape: "ring", text: "Pool error" });
70
64
  throw error;
71
65
  }
72
66
  }
73
- // Quoi qu'il arrive, on demande une connexion au pool (qui pourrait être fraîchement créé)
74
67
  try {
75
68
  return await this.pool.connect();
76
69
  } 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
- });
70
+ this.error(`Error connecting to pool: ${poolConnectError}`, poolConnectError);
71
+ this.status({ fill: "red", shape: "ring", text: "Pool connect err" });
86
72
  throw poolConnectError;
87
73
  }
88
74
  };
89
75
 
90
- // --- NOUVELLE MÉTHODE: resetPool ---
76
+ this.getFreshConnectionConfig = function() {
77
+ return {
78
+ connectionString: this._buildConnectionString(),
79
+ connectionTimeout: parseInt(this.config.connectionTimeout) || 0,
80
+ loginTimeout: parseInt(this.config.loginTimeout) || 0,
81
+ };
82
+ };
83
+
91
84
  this.resetPool = async () => {
92
- if (this.pool) {
85
+ if (this.pool) {
93
86
  this.log("Resetting connection pool.");
94
- this.status({
95
- fill: "yellow",
96
- shape: "ring",
97
- text: "Resetting pool...",
98
- });
87
+ this.status({ fill: "yellow", shape: "ring", text: "Resetting pool..." });
99
88
  try {
100
89
  await this.pool.close();
101
90
  this.log("Connection pool closed successfully for reset.");
102
91
  } 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
92
+ this.error(`Error closing pool during reset: ${closeError}`, closeError);
108
93
  } finally {
109
94
  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()
95
+ this.connecting = false;
112
96
  }
113
97
  } else {
114
98
  this.log("Pool reset requested, but no active pool to reset.");
@@ -116,606 +100,347 @@ module.exports = function (RED) {
116
100
  };
117
101
 
118
102
  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
103
  this.log("Closing ODBC config node. Attempting to close pool.");
122
104
  if (this.pool) {
123
105
  try {
124
106
  await this.pool.close();
125
- this.log(
126
- "Connection pool closed successfully on node close."
127
- );
107
+ this.log("Connection pool closed successfully on node close.");
128
108
  this.pool = null;
129
109
  } catch (error) {
130
- this.error(
131
- `Error closing connection pool on node close: ${error}`,
132
- error
133
- );
110
+ this.error(`Error closing connection pool on node close: ${error}`, error);
134
111
  }
135
112
  }
136
113
  done();
137
114
  });
138
115
  }
139
116
 
140
- RED.nodes.registerType("odbc config", poolConfig);
117
+ RED.nodes.registerType("odbc config", poolConfig, {
118
+ credentials: {
119
+ password: { type: "password" }
120
+ }
121
+ });
122
+
123
+ RED.httpAdmin.post("/odbc_config/:id/test", RED.auth.needsPermission("odbc.write"), async function(req, res) {
124
+ const tempConfig = req.body;
125
+ const tempCredentials = { password: tempConfig.password };
126
+ delete tempConfig.password;
127
+
128
+ const buildTestConnectionString = () => {
129
+ if (tempConfig.connectionMode === 'structured') {
130
+ if (!tempConfig.dbType || !tempConfig.server) return res.status(400).send("Mode structuré : le type de BD et le serveur sont requis.");
131
+ let driver;
132
+ let parts = [];
133
+ switch (tempConfig.dbType) {
134
+ case 'sqlserver': driver = 'ODBC Driver 17 for SQL Server'; break;
135
+ case 'postgresql': driver = 'PostgreSQL Unicode'; break;
136
+ case 'mysql': driver = 'MySQL ODBC 8.0 Unicode Driver'; break;
137
+ default: driver = tempConfig.driver || ''; break;
138
+ }
139
+ if(driver) parts.unshift(`DRIVER={${driver}}`);
140
+ parts.push(`SERVER=${tempConfig.server}`);
141
+ if (tempConfig.database) parts.push(`DATABASE=${tempConfig.database}`);
142
+ if (tempConfig.user) parts.push(`UID=${tempConfig.user}`);
143
+ if (tempCredentials.password) parts.push(`PWD=${tempCredentials.password}`);
144
+ return parts.join(';');
145
+ } else {
146
+ let connStr = tempConfig.connectionString || "";
147
+ if (tempCredentials.password && connStr.includes('{{{password}}}')) {
148
+ connStr = connStr.replace('{{{password}}}', tempCredentials.password);
149
+ }
150
+ return connStr;
151
+ }
152
+ };
153
+
154
+ let connection;
155
+ try {
156
+ const testConnectionString = buildTestConnectionString();
157
+ if (!testConnectionString) return res.status(400).send("La chaîne de connexion est vide.");
158
+ connection = await odbcModule.connect(testConnectionString);
159
+ res.sendStatus(200);
160
+ } catch (err) {
161
+ res.status(500).send(err.message || "Erreur inconnue durant le test.");
162
+ } finally {
163
+ if (connection) await connection.close();
164
+ }
165
+ });
141
166
 
142
167
  // --- ODBC Query Node ---
143
168
  function odbc(config) {
144
169
  RED.nodes.createNode(this, config);
145
170
  this.config = config;
146
- this.poolNode = RED.nodes.getNode(this.config.connection); // C'est le noeud 'odbc config'
171
+ this.poolNode = RED.nodes.getNode(this.config.connection);
147
172
  this.name = this.config.name;
173
+ this.isAwaitingRetry = false;
174
+ this.retryTimer = null;
148
175
 
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
- ) => {
176
+ this.enhanceError = (error, query, params, defaultMessage = "Query error") => {
156
177
  const queryContext = (() => {
157
178
  let s = "";
158
179
  if (query || params) {
159
180
  s += " {";
160
- if (query)
161
- s += `"query": '${query.substring(0, 100)}${
162
- query.length > 100 ? "..." : ""
163
- }'`; // Tronquer les longues requêtes
181
+ if (query) s += `"query": '${query.substring(0, 100)}${query.length > 100 ? "..." : ""}'`;
164
182
  if (params) s += `, "params": '${JSON.stringify(params)}'`;
165
183
  s += "}";
166
184
  return s;
167
185
  }
168
186
  return "";
169
187
  })();
170
-
171
188
  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
-
189
+ if (typeof error === "object" && error !== null && error.message) { finalError = error; }
190
+ else if (typeof error === "string") { finalError = new Error(error); }
191
+ else { finalError = new Error(defaultMessage); }
180
192
  finalError.message = `${finalError.message}${queryContext}`;
181
193
  if (query) finalError.query = query;
182
194
  if (params) finalError.params = params;
183
-
184
195
  return finalError;
185
196
  };
186
197
 
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
- ) => {
198
+ this.executeQueryAndProcess = async (dbConnection, queryString, queryParams, isPreparedStatement, msg) => {
196
199
  let result;
197
- // Exécution de la requête
198
200
  if (isPreparedStatement) {
199
201
  const stmt = await dbConnection.createStatement();
200
202
  try {
201
203
  await stmt.prepare(queryString);
202
- await stmt.bind(queryParams); // queryParams vient de msg.parameters
204
+ await stmt.bind(queryParams);
203
205
  result = await stmt.execute();
204
206
  } 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
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
- }
208
+ try { await stmt.close(); } catch (stmtCloseError) { this.warn(`Error closing statement: ${stmtCloseError}`); }
215
209
  }
216
210
  }
217
211
  } else {
218
- result = await dbConnection.query(queryString, queryParams); // queryParams ici aussi
212
+ result = await dbConnection.query(queryString, queryParams);
219
213
  }
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
214
+ if (typeof result === "undefined") { throw new Error("Query returned undefined."); }
230
215
  const newMsg = RED.util.cloneMessage(msg);
231
-
232
216
  const otherParams = {};
233
217
  let actualDataRows = [];
234
-
235
218
  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
219
  if (Array.isArray(result)) {
239
- actualDataRows = [...result]; // Copie des lignes
220
+ actualDataRows = [...result];
240
221
  for (const [key, value] of Object.entries(result)) {
241
- if (isNaN(parseInt(key))) {
242
- otherParams[key] = value;
243
- }
222
+ if (isNaN(parseInt(key))) { otherParams[key] = value; }
244
223
  }
245
224
  } 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
- }
225
+ for (const [key, value] of Object.entries(result)) { otherParams[key] = value; }
251
226
  }
252
227
  }
253
-
254
228
  const columnMetadata = otherParams.columns;
255
- if (
256
- Array.isArray(columnMetadata) &&
257
- Array.isArray(actualDataRows) &&
258
- actualDataRows.length > 0
259
- ) {
229
+ if (Array.isArray(columnMetadata) && Array.isArray(actualDataRows) && actualDataRows.length > 0) {
260
230
  const sqlBitColumnNames = new Set();
261
231
  columnMetadata.forEach((col) => {
262
- if (
263
- col &&
264
- typeof col.name === "string" &&
265
- col.dataTypeName === "SQL_BIT"
266
- ) {
232
+ if (col && typeof col.name === "string" && col.dataTypeName === "SQL_BIT") {
267
233
  sqlBitColumnNames.add(col.name);
268
234
  }
269
235
  });
270
-
271
236
  if (sqlBitColumnNames.size > 0) {
272
237
  actualDataRows.forEach((row) => {
273
238
  if (typeof row === "object" && row !== null) {
274
239
  for (const columnName of sqlBitColumnNames) {
275
240
  if (row.hasOwnProperty(columnName)) {
276
241
  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
- }
242
+ if (value === "1" || value === 1) { row[columnName] = true; }
243
+ else if (value === "0" || value === 0) { row[columnName] = false; }
282
244
  }
283
245
  }
284
246
  }
285
247
  });
286
248
  }
287
249
  }
288
-
289
250
  objPath.set(newMsg, this.config.outputObj, actualDataRows);
290
-
291
251
  if (this.poolNode?.parser && queryString) {
292
252
  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
- );
253
+ newMsg.parsedQuery = this.poolNode.parser.astify(structuredClone(queryString));
297
254
  } catch (syntaxError) {
298
- this.warn(
299
- `Could not parse query for parsedQuery output: ${syntaxError}`
300
- );
255
+ this.warn(`Could not parse query for parsedQuery output: ${syntaxError}`);
301
256
  }
302
257
  }
303
-
304
- if (Object.keys(otherParams).length) {
305
- newMsg.odbc = otherParams;
306
- }
258
+ if (Object.keys(otherParams).length) { newMsg.odbc = otherParams; }
307
259
  return newMsg;
308
260
  };
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
-
261
+
262
+ this.executeStreamQuery = async (dbConnection, queryString, queryParams, msg, send, done) => {
263
+ const chunkSize = parseInt(this.config.streamChunkSize) || 1;
264
+ let cursor;
265
+ let rowCount = 0;
266
+ let chunk = [];
267
+
316
268
  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)) {
335
- if (parsed[0] === "name" || parsed[0] === "&") {
336
- if (!objPath.has(msg, parsed[1])) {
337
- this.warn(
338
- `Mustache parameter "${parsed[1]}" is absent and will render to undefined`
339
- );
340
- }
341
- }
269
+ cursor = await dbConnection.cursor(queryString, queryParams);
270
+ this.status({ fill: "blue", shape: "dot", text: "streaming rows..." });
271
+ let row = await cursor.fetch();
272
+ while (row) {
273
+ rowCount++;
274
+ chunk.push(row);
275
+ if (chunk.length >= chunkSize) {
276
+ const newMsg = RED.util.cloneMessage(msg);
277
+ objPath.set(newMsg, this.config.outputObj, chunk);
278
+ newMsg.odbc_stream = { index: rowCount - chunk.length, count: chunk.length, complete: false };
279
+ send(newMsg);
280
+ chunk = [];
342
281
  }
343
- currentQueryString = mustache.render(
344
- currentQueryString,
345
- msg
346
- );
282
+ row = await cursor.fetch();
347
283
  }
348
-
349
- if (msg?.query) {
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
- );
362
- }
363
- currentQueryString = msg.query;
364
- } else if (msg?.payload) {
365
- // Ensuite msg.payload.query ou msg.payload (si string)
366
- if (typeof msg.payload === "string") {
367
- try {
368
- const payloadJson = JSON.parse(msg.payload);
369
- if (
370
- payloadJson?.query &&
371
- typeof payloadJson.query === "string"
372
- ) {
373
- currentQueryString = payloadJson.query;
374
- }
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;
383
- }
284
+ if (chunk.length > 0) {
285
+ const newMsg = RED.util.cloneMessage(msg);
286
+ objPath.set(newMsg, this.config.outputObj, chunk);
287
+ newMsg.odbc_stream = { index: rowCount - chunk.length, count: chunk.length, complete: true };
288
+ send(newMsg);
384
289
  }
385
-
386
- if (!currentQueryString) {
387
- throw new Error("No query to execute");
290
+ if (rowCount === 0) {
291
+ const newMsg = RED.util.cloneMessage(msg);
292
+ objPath.set(newMsg, this.config.outputObj, []);
293
+ newMsg.odbc_stream = { index: 0, count: 0, complete: true };
294
+ send(newMsg);
388
295
  }
296
+ this.status({ fill: "green", shape: "dot", text: `success (${rowCount} rows)` });
297
+ if(done) done();
298
+ } catch(err) {
299
+ throw err;
300
+ }
301
+ finally {
302
+ if (cursor) await cursor.close();
303
+ }
304
+ };
389
305
 
390
- // Re-vérifier isPreparedStatement si query a changé, et valider les paramètres
391
- isPreparedStatement =
392
- currentQueryParams ||
393
- (currentQueryString && currentQueryString.includes("?"));
306
+ this.runQuery = async (msg, send, done) => {
307
+ let currentQueryString = this.config.query || "";
308
+ let currentQueryParams = msg.parameters;
309
+ let isPreparedStatement = false;
310
+ let connectionFromPool = null;
394
311
 
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.
312
+ try {
313
+ this.status({ fill: "blue", shape: "dot", text: "preparing..." });
314
+ this.config.outputObj = msg?.output || this.config?.outputObj || "payload";
315
+
316
+ isPreparedStatement = currentQueryParams || (currentQueryString && currentQueryString.includes("?"));
317
+ if (!isPreparedStatement && currentQueryString) {
318
+ for (const parsed of mustache.parse(currentQueryString)) {
319
+ if ((parsed[0] === "name" || parsed[0] === "&") && !objPath.has(msg, parsed[1])) {
320
+ this.warn(`Mustache parameter "${parsed[1]}" is absent.`);
428
321
  }
429
322
  }
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
- );
438
- }
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
- );
447
- }
323
+ currentQueryString = mustache.render(currentQueryString, msg);
448
324
  }
325
+ if (msg?.query) { currentQueryString = msg.query; }
326
+ if (!currentQueryString) { throw new Error("No query to execute"); }
449
327
 
450
- // Validation du champ de sortie
451
- if (!this.config.outputObj) {
452
- throw new Error(
453
- "Invalid output object definition (outputObj is empty)"
454
- );
455
- }
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
- );
467
- }
328
+ const execute = async (conn) => {
329
+ if (this.config.streaming) {
330
+ await this.executeStreamQuery(conn, currentQueryString, currentQueryParams, msg, send, done);
331
+ } else {
332
+ const processedMsg = await this.executeQueryAndProcess(conn, currentQueryString, currentQueryParams, isPreparedStatement, msg);
333
+ this.status({ fill: "green", shape: "dot", text: "success" });
334
+ send(processedMsg);
335
+ if(done) done();
336
+ }
337
+ };
468
338
 
469
- // --- Première tentative avec une connexion du pool ---
470
339
  let firstAttemptError = null;
471
340
  try {
472
341
  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
342
+ await execute(connectionFromPool);
343
+ return;
500
344
  } 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
345
+ firstAttemptError = this.enhanceError(err, currentQueryString, currentQueryParams, "Query failed with pooled connection");
346
+ this.warn(`First attempt failed: ${firstAttemptError.message}`);
511
347
  } 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;
521
- }
348
+ if (connectionFromPool) await connectionFromPool.close();
522
349
  }
523
350
 
524
- // --- Si la première tentative a échoué (firstAttemptError est défini) ---
525
351
  if (firstAttemptError) {
526
- if (
527
- this.poolNode &&
528
- this.poolNode.config.retryFreshConnection
529
- ) {
352
+ if (this.poolNode && this.poolNode.config.retryFreshConnection) {
530
353
  this.log("Attempting retry with a fresh connection.");
531
- this.status({
532
- fill: "yellow",
533
- shape: "dot",
534
- text: "Retrying (fresh)...",
535
- });
536
-
354
+ this.status({ fill: "yellow", shape: "dot", text: "Retrying (fresh)..." });
537
355
  let freshConnection = null;
538
356
  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
- );
357
+ const freshConnectConfig = this.poolNode.getFreshConnectionConfig();
358
+ freshConnection = await odbcModule.connect(freshConnectConfig);
553
359
  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
360
+ await execute(freshConnection);
361
+ this.log("Query successful with fresh connection. Resetting pool.");
362
+ await this.poolNode.resetPool();
363
+ return;
364
+ } catch (freshError) {
365
+ this.warn(`Retry with fresh connection also failed: ${freshError.message}`);
366
+ const retryDelay = parseInt(this.poolNode.config.retryDelay) || 0;
367
+ if (retryDelay > 0) {
368
+ this.isAwaitingRetry = true;
369
+ this.status({ fill: "red", shape: "ring", text: `Retry in ${retryDelay}s...` });
370
+ this.log(`Scheduling retry in ${retryDelay} seconds.`);
371
+ this.retryTimer = setTimeout(() => {
372
+ this.isAwaitingRetry = false;
373
+ this.log("Timer expired. Triggering scheduled retry.");
374
+ this.receive(msg);
375
+ }, retryDelay * 1000);
376
+ if (done) done();
576
377
  } else {
577
- this.warn(
578
- "poolNode.resetPool is not available. Pool cannot be reset automatically."
579
- );
378
+ throw this.enhanceError(freshError, currentQueryString, currentQueryParams, "Query failed on fresh connection retry");
580
379
  }
581
-
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
380
  } 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
- }
605
- }
381
+ if (freshConnection) await freshConnection.close();
606
382
  }
607
383
  } else {
608
- // retryFreshConnection n'est pas activé, donc on lance l'erreur de la première tentative
609
384
  throw firstAttemptError;
610
385
  }
611
386
  }
612
- } catch (err) {
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
-
627
- if (done) {
628
- done(finalError); // Passer l'erreur au callback done de Node-RED
629
- } else {
630
- this.error(finalError, msg); // Utiliser this.error pour logguer l'erreur correctement
631
- }
632
- }
633
- }; // Fin de runQuery
387
+ } catch (err) {
388
+ const finalError = err instanceof Error ? err : new Error(String(err));
389
+ this.status({ fill: "red", shape: "ring", text: "query error" });
390
+ if (done) { done(finalError); } else { this.error(finalError, msg); }
391
+ }
392
+ };
634
393
 
635
394
  this.checkPool = async function (msg, send, done) {
636
395
  try {
637
- if (!this.poolNode) {
638
- throw new Error(
639
- "ODBC Connection Configuration node is not properly configured or deployed."
640
- );
641
- }
396
+ if (!this.poolNode) { throw new Error("ODBC Config node not properly configured."); }
642
397
  if (this.poolNode.connecting) {
643
- // Si le pool est en cours d'initialisation
644
398
  this.warn("Waiting for connection pool to initialize...");
645
- this.status({
646
- fill: "yellow",
647
- shape: "ring",
648
- text: "Waiting for pool",
649
- });
399
+ this.status({ fill: "yellow", shape: "ring", text: "Waiting for pool" });
650
400
  setTimeout(() => {
651
401
  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
- }
402
+ this.status({ fill: "red", shape: "dot", text: "Pool wait failed" });
403
+ if (done) { done(err); } else { this.error(err, msg); }
663
404
  });
664
- }, 1000); // Réessayer après 1 seconde
405
+ }, 1000);
665
406
  return;
666
407
  }
667
-
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()
671
-
672
408
  await this.runQuery(msg, send, done);
673
409
  } catch (err) {
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
- });
685
- if (done) {
686
- done(finalError);
687
- } else {
688
- this.error(finalError, msg);
689
- }
410
+ const finalError = err instanceof Error ? err : new Error(String(err));
411
+ this.status({ fill: "red", shape: "dot", text: "Op failed" });
412
+ if (done) { done(finalError); } else { this.error(finalError, msg); }
690
413
  }
691
414
  };
692
415
 
693
416
  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
417
+ if (this.isAwaitingRetry) {
418
+ if (this.poolNode && this.poolNode.config.retryOnMsg) {
419
+ this.log("New message received, overriding retry timer and attempting query now.");
420
+ clearTimeout(this.retryTimer);
421
+ this.retryTimer = null;
422
+ this.isAwaitingRetry = false;
423
+ } else {
424
+ this.warn("Node is in a retry-wait state. New message ignored as per configuration.");
425
+ if (done) done();
426
+ return;
427
+ }
428
+ }
695
429
  try {
696
430
  await this.checkPool(msg, send, done);
697
431
  } 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);
709
- }
432
+ const finalError = error instanceof Error ? error : new Error(String(error));
433
+ this.status({ fill: "red", shape: "ring", text: "Input error" });
434
+ if (done) { done(finalError); } else { this.error(finalError, msg); }
710
435
  }
711
436
  });
712
437
 
713
438
  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
439
+ if (this.retryTimer) {
440
+ clearTimeout(this.retryTimer);
441
+ this.log("Cleared pending retry timer on node close/redeploy.");
442
+ }
443
+ this.status({});
719
444
  done();
720
445
  });
721
446
 
@@ -723,11 +448,9 @@ module.exports = function (RED) {
723
448
  this.status({ fill: "green", shape: "dot", text: "ready" });
724
449
  } else {
725
450
  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
- );
451
+ this.warn("ODBC Config node not found or not deployed.");
729
452
  }
730
453
  }
731
454
 
732
455
  RED.nodes.registerType("odbc", odbc);
733
- };
456
+ };