@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.
- package/README.md +63 -107
- package/odbc.html +224 -222
- package/odbc.js +380 -228
- package/package.json +26 -7
package/odbc.js
CHANGED
|
@@ -1,63 +1,168 @@
|
|
|
1
|
-
module.exports = function(RED) {
|
|
2
|
-
const odbcModule = require(
|
|
3
|
-
const mustache = require(
|
|
4
|
-
const objPath = require(
|
|
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);
|
|
9
|
-
this.config = config;
|
|
10
|
-
this.pool = null;
|
|
11
|
-
this.connecting = false;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38
|
-
this.error(`Error creating connection pool: ${error}
|
|
39
|
-
this.status({ fill: "red", shape: "ring", text: "
|
|
40
|
-
throw error;
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
if (
|
|
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
|
-
|
|
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(
|
|
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.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
190
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
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: "
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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(
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
|
|
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.
|
|
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);
|