@bkmj/node-red-contrib-odbcmj 1.6.6 → 2.0.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 +63 -107
  2. package/odbc.html +224 -222
  3. package/odbc.js +380 -228
  4. package/package.json +26 -7
package/odbc.js CHANGED
@@ -1,63 +1,168 @@
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"); // 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
- 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
12
-
13
- const enableSyntaxChecker = this.config.syntaxtick; // Renamed for clarity
14
- const syntax = this.config.syntax;
15
- delete this.config.syntaxtick;
16
- delete this.config.syntax;
17
-
18
- // Create a SQL parser if syntax check is enabled
19
- this.parser = enableSyntaxChecker
20
- ? new require('node-sql-parser/build/' + syntax).Parser()
21
- : null;
22
-
23
- // Convert numeric config params to integers
24
- for (const [key, value] of Object.entries(this.config)) {
25
- if (!isNaN(parseInt(value))) {
26
- this.config[key] = parseInt(value);
8
+ RED.nodes.createNode(this, config);
9
+ this.config = config;
10
+ this.pool = null;
11
+ this.connecting = false;
12
+
13
+ this.credentials = RED.nodes.getCredentials(this.id);
14
+
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;
27
40
  }
28
- }
41
+ };
29
42
 
30
- // Connect to the database and create a connection pool
31
43
  this.connect = async () => {
32
44
  if (!this.pool) {
45
+ this.connecting = true;
46
+ this.status({ fill: "yellow", shape: "dot", text: "Pool init..." });
33
47
  try {
34
- this.pool = await odbcModule.pool(this.config);
48
+ const finalConnectionString = this._buildConnectionString();
49
+ if (!finalConnectionString) throw new Error("La chaîne de connexion est vide.");
50
+
51
+ const poolParams = { ...this.config };
52
+ poolParams.connectionString = finalConnectionString;
53
+
54
+ ['retryFreshConnection', 'retryDelay', 'retryOnMsg', 'syntax', 'connectionMode', 'dbType', 'server', 'database', 'user', 'driver'].forEach(k => delete poolParams[k]);
55
+
56
+ this.pool = await odbcModule.pool(poolParams);
35
57
  this.connecting = false;
58
+ this.status({ fill: "green", shape: "dot", text: "Pool ready" });
59
+ this.log("Connection pool initialized successfully.");
36
60
  } 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
61
+ this.connecting = false;
62
+ this.error(`Error creating connection pool: ${error.message}`, error);
63
+ this.status({ fill: "red", shape: "ring", text: "Pool error" });
64
+ throw error;
65
+ }
66
+ }
67
+ try {
68
+ return await this.pool.connect();
69
+ } catch (poolConnectError) {
70
+ this.error(`Error connecting to pool: ${poolConnectError}`, poolConnectError);
71
+ this.status({ fill: "red", shape: "ring", text: "Pool connect err" });
72
+ throw poolConnectError;
73
+ }
74
+ };
75
+
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
+
84
+ this.resetPool = async () => {
85
+ if (this.pool) {
86
+ this.log("Resetting connection pool.");
87
+ this.status({ fill: "yellow", shape: "ring", text: "Resetting pool..." });
88
+ try {
89
+ await this.pool.close();
90
+ this.log("Connection pool closed successfully for reset.");
91
+ } catch (closeError) {
92
+ this.error(`Error closing pool during reset: ${closeError}`, closeError);
93
+ } finally {
94
+ this.pool = null;
95
+ this.connecting = false;
41
96
  }
97
+ } else {
98
+ this.log("Pool reset requested, but no active pool to reset.");
42
99
  }
43
- return await this.pool.connect();
44
100
  };
45
101
 
46
- // Close the connection pool when the node is closed
47
- this.on('close', async (removed, done) => {
48
- if (removed && this.pool) {
102
+ this.on("close", async (removed, done) => {
103
+ this.log("Closing ODBC config node. Attempting to close pool.");
104
+ if (this.pool) {
49
105
  try {
50
106
  await this.pool.close();
107
+ this.log("Connection pool closed successfully on node close.");
108
+ this.pool = null;
51
109
  } catch (error) {
52
- // Handle errors during pool closure
53
- this.error(`Error closing connection pool: ${error}`);
110
+ this.error(`Error closing connection pool on node close: ${error}`, error);
54
111
  }
55
112
  }
56
113
  done();
57
114
  });
58
115
  }
59
116
 
60
- 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
+ });
61
166
 
62
167
  // --- ODBC Query Node ---
63
168
  function odbc(config) {
@@ -65,239 +170,286 @@ module.exports = function(RED) {
65
170
  this.config = config;
66
171
  this.poolNode = RED.nodes.getNode(this.config.connection);
67
172
  this.name = this.config.name;
68
-
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;
173
+ this.isAwaitingRetry = false;
174
+ this.retryTimer = null;
175
+
176
+ this.enhanceError = (error, query, params, defaultMessage = "Query error") => {
177
+ const queryContext = (() => {
178
+ let s = "";
179
+ if (query || params) {
180
+ s += " {";
181
+ if (query) s += `"query": '${query.substring(0, 100)}${query.length > 100 ? "..." : ""}'`;
182
+ if (params) s += `, "params": '${JSON.stringify(params)}'`;
183
+ s += "}";
184
+ return s;
80
185
  }
186
+ return "";
187
+ })();
188
+ let finalError;
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); }
192
+ finalError.message = `${finalError.message}${queryContext}`;
193
+ if (query) finalError.query = query;
194
+ if (params) finalError.params = params;
195
+ return finalError;
196
+ };
81
197
 
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)) {
86
- if (parsed[0] === "name" || parsed[0] === "&") {
87
- if (!objPath.has(msg, parsed[1])) {
88
- this.warn(`Mustache parameter "${parsed[1]}" is absent and will render to undefined`);
89
- }
90
- }
198
+ this.executeQueryAndProcess = async (dbConnection, queryString, queryParams, isPreparedStatement, msg) => {
199
+ let result;
200
+ if (isPreparedStatement) {
201
+ const stmt = await dbConnection.createStatement();
202
+ try {
203
+ await stmt.prepare(queryString);
204
+ await stmt.bind(queryParams);
205
+ result = await stmt.execute();
206
+ } finally {
207
+ if (stmt && typeof stmt.close === "function") {
208
+ try { await stmt.close(); } catch (stmtCloseError) { this.warn(`Error closing statement: ${stmtCloseError}`); }
91
209
  }
92
- this.queryString = mustache.render(this.queryString, msg);
93
210
  }
94
-
95
- // Handle cases where the query is provided in the message
96
- if (msg?.query) {
97
- if (this.queryString) {
98
- this.log('Warning. The query defined in the node configuration was overwritten by msg.config.');
99
- }
100
- this.queryString = msg.query;
101
- } else if (msg?.payload) {
102
- if (typeof msg.payload === 'string') {
103
- try {
104
- const payloadJson = JSON.parse(msg.payload);
105
- if (payloadJson?.query && typeof payloadJson.query === 'string') {
106
- this.queryString = payloadJson.query;
107
- }
108
- } catch (err) {} // Ignore JSON parsing errors
109
- } else if (msg.payload?.query && typeof msg.payload.query === 'string') {
110
- this.queryString = msg.payload.query;
211
+ } else {
212
+ result = await dbConnection.query(queryString, queryParams);
213
+ }
214
+ if (typeof result === "undefined") { throw new Error("Query returned undefined."); }
215
+ const newMsg = RED.util.cloneMessage(msg);
216
+ const otherParams = {};
217
+ let actualDataRows = [];
218
+ if (result !== null && typeof result === "object") {
219
+ if (Array.isArray(result)) {
220
+ actualDataRows = [...result];
221
+ for (const [key, value] of Object.entries(result)) {
222
+ if (isNaN(parseInt(key))) { otherParams[key] = value; }
111
223
  }
224
+ } else {
225
+ for (const [key, value] of Object.entries(result)) { otherParams[key] = value; }
112
226
  }
113
-
114
- if (!this.queryString) {
115
- throw new Error("No query to execute");
116
- }
117
-
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());
127
-
128
- // Create an ordered array of values
129
- msg.parameters = paramNames.map(name => msg.parameters[name]);
130
- }
131
- }
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");
227
+ }
228
+ const columnMetadata = otherParams.columns;
229
+ if (Array.isArray(columnMetadata) && Array.isArray(actualDataRows) && actualDataRows.length > 0) {
230
+ const sqlBitColumnNames = new Set();
231
+ columnMetadata.forEach((col) => {
232
+ if (col && typeof col.name === "string" && col.dataTypeName === "SQL_BIT") {
233
+ sqlBitColumnNames.add(col.name);
138
234
  }
235
+ });
236
+ if (sqlBitColumnNames.size > 0) {
237
+ actualDataRows.forEach((row) => {
238
+ if (typeof row === "object" && row !== null) {
239
+ for (const columnName of sqlBitColumnNames) {
240
+ if (row.hasOwnProperty(columnName)) {
241
+ const value = row[columnName];
242
+ if (value === "1" || value === 1) { row[columnName] = true; }
243
+ else if (value === "0" || value === 0) { row[columnName] = false; }
244
+ }
245
+ }
246
+ }
247
+ });
139
248
  }
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");
249
+ }
250
+ objPath.set(newMsg, this.config.outputObj, actualDataRows);
251
+ if (this.poolNode?.parser && queryString) {
252
+ try {
253
+ newMsg.parsedQuery = this.poolNode.parser.astify(structuredClone(queryString));
254
+ } catch (syntaxError) {
255
+ this.warn(`Could not parse query for parsedQuery output: ${syntaxError}`);
256
+ }
257
+ }
258
+ if (Object.keys(otherParams).length) { newMsg.odbc = otherParams; }
259
+ return newMsg;
260
+ };
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
+
268
+ try {
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 = [];
147
281
  }
282
+ row = await cursor.fetch();
148
283
  }
149
-
150
- // --- Output object validation ---
151
- if (!this.config.outputObj) {
152
- throw new Error("Invalid output object definition");
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);
153
289
  }
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");
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);
160
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
+ };
161
305
 
162
- // --- Get a connection from the pool ---
163
- try {
164
- this.connection = await this.poolNode.connect();
165
- if (!this.connection) {
166
- throw new Error("No connection available");
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;
311
+
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.`);
321
+ }
167
322
  }
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
323
+ currentQueryString = mustache.render(currentQueryString, msg);
173
324
  }
325
+ if (msg?.query) { currentQueryString = msg.query; }
326
+ if (!currentQueryString) { throw new Error("No query to execute"); }
174
327
 
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();
328
+ const execute = async (conn) => {
329
+ if (this.config.streaming) {
330
+ await this.executeStreamQuery(conn, currentQueryString, currentQueryParams, msg, send, done);
188
331
  } else {
189
- // --- Execute regular query ---
190
- result = await this.connection.query(this.queryString, msg?.parameters);
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();
191
336
  }
337
+ };
192
338
 
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];
339
+ let firstAttemptError = null;
340
+ try {
341
+ connectionFromPool = await this.poolNode.connect();
342
+ await execute(connectionFromPool);
343
+ return;
344
+ } catch (err) {
345
+ firstAttemptError = this.enhanceError(err, currentQueryString, currentQueryParams, "Query failed with pooled connection");
346
+ this.warn(`First attempt failed: ${firstAttemptError.message}`);
347
+ } finally {
348
+ if (connectionFromPool) await connectionFromPool.close();
349
+ }
350
+
351
+ if (firstAttemptError) {
352
+ if (this.poolNode && this.poolNode.config.retryFreshConnection) {
353
+ this.log("Attempting retry with a fresh connection.");
354
+ this.status({ fill: "yellow", shape: "dot", text: "Retrying (fresh)..." });
355
+ let freshConnection = null;
356
+ try {
357
+ const freshConnectConfig = this.poolNode.getFreshConnectionConfig();
358
+ freshConnection = await odbcModule.connect(freshConnectConfig);
359
+ this.log("Fresh connection established for retry.");
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();
377
+ } else {
378
+ throw this.enhanceError(freshError, currentQueryString, currentQueryParams, "Query failed on fresh connection retry");
200
379
  }
380
+ } finally {
381
+ if (freshConnection) await freshConnection.close();
201
382
  }
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
383
  } 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
- return str;
223
- }
224
- })()
225
- if(typeof error == 'object'){
226
- // Enhance the error object with query information
227
-
228
- if(this?.queryString) error.query = this.queryString;
229
- if(this?.parameters) error.params = msg.parameters;
230
-
231
- if(error?.message){
232
- error.message += str;
233
- }
234
- }
235
- else if (typeof error == 'string'){
236
- error += str;
384
+ throw firstAttemptError;
237
385
  }
238
- // Handle query errors
239
- this.status({ fill: "red", shape: "ring", text: "Query error" });
240
- throw error; // Re-throw to trigger the outer catch block
241
- } finally {
242
- await this.connection.close();
243
- }
244
-
245
- if (done) {
246
- done();
247
386
  }
248
- } catch (err) {
249
- this.status({ fill: "red", shape: "ring", text: err.message || "query error" });
250
- if (done) {
251
- done(err);
252
- } else {
253
- this.error(err, msg);
254
- }
255
- }
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
+ }
256
392
  };
257
393
 
258
- // --- Check connection pool before running query ---
259
- this.checkPool = async function(msg, send, done) {
394
+ this.checkPool = async function (msg, send, done) {
260
395
  try {
396
+ if (!this.poolNode) { throw new Error("ODBC Config node not properly configured."); }
261
397
  if (this.poolNode.connecting) {
262
- this.warn("Waiting for connection pool...");
263
- this.status({ fill: "yellow", shape: "ring", text: "requesting pool" });
398
+ this.warn("Waiting for connection pool to initialize...");
399
+ this.status({ fill: "yellow", shape: "ring", text: "Waiting for pool" });
264
400
  setTimeout(() => {
265
- this.checkPool(msg, send, done);
401
+ this.checkPool(msg, send, done).catch((err) => {
402
+ this.status({ fill: "red", shape: "dot", text: "Pool wait failed" });
403
+ if (done) { done(err); } else { this.error(err, msg); }
404
+ });
266
405
  }, 1000);
267
406
  return;
268
407
  }
269
-
270
- if (!this.poolNode.pool) {
271
- this.poolNode.connecting = true;
272
- }
273
-
274
408
  await this.runQuery(msg, send, done);
275
409
  } catch (err) {
276
- this.status({ fill: "red", shape: "dot", text: "operation failed" });
277
- if (done) {
278
- done(err);
279
- } else {
280
- this.error(err, msg);
281
- }
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); }
282
413
  }
283
414
  };
284
415
 
285
- this.on('input', this.checkPool);
286
-
287
- // --- Close the connection when the node is closed ---
288
- this.on('close', async (done) => {
289
- if (this.connection) {
290
- try {
291
- await this.connection.close();
292
- } catch (error) {
293
- // Handle connection close errors
294
- this.error(`Error closing connection: ${error}`);
416
+ this.on("input", async (msg, send, done) => {
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;
295
427
  }
296
428
  }
429
+ try {
430
+ await this.checkPool(msg, send, done);
431
+ } catch (error) {
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); }
435
+ }
436
+ });
437
+
438
+ this.on("close", async (done) => {
439
+ if (this.retryTimer) {
440
+ clearTimeout(this.retryTimer);
441
+ this.log("Cleared pending retry timer on node close/redeploy.");
442
+ }
443
+ this.status({});
297
444
  done();
298
445
  });
299
446
 
300
- this.status({ fill: 'green', shape: 'dot', text: 'ready' });
447
+ if (this.poolNode) {
448
+ this.status({ fill: "green", shape: "dot", text: "ready" });
449
+ } else {
450
+ this.status({ fill: "red", shape: "ring", text: "No config node" });
451
+ this.warn("ODBC Config node not found or not deployed.");
452
+ }
301
453
  }
302
454
 
303
455
  RED.nodes.registerType("odbc", odbc);