@bkmj/node-red-contrib-odbcmj 2.3.1 → 2.4.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.
- package/odbc.js +191 -185
- package/package.json +1 -1
package/odbc.js
CHANGED
|
@@ -9,14 +9,12 @@ module.exports = function (RED) {
|
|
|
9
9
|
this.config = config;
|
|
10
10
|
this.pool = null;
|
|
11
11
|
this.connecting = false;
|
|
12
|
-
|
|
13
12
|
this.credentials = RED.nodes.getCredentials(this.id);
|
|
14
13
|
|
|
15
14
|
this.config.queryTimeoutSeconds = parseInt(config.queryTimeoutSeconds, 10);
|
|
16
15
|
if (isNaN(this.config.queryTimeoutSeconds) || this.config.queryTimeoutSeconds < 0) {
|
|
17
16
|
this.config.queryTimeoutSeconds = 0;
|
|
18
17
|
}
|
|
19
|
-
|
|
20
18
|
this.closeOperationTimeout = 10000; // 10 secondes
|
|
21
19
|
|
|
22
20
|
this._buildConnectionString = function() {
|
|
@@ -39,8 +37,7 @@ module.exports = function (RED) {
|
|
|
39
37
|
if (this.credentials && this.credentials.password) parts.push(`PWD=${this.credentials.password}`);
|
|
40
38
|
return parts.join(';');
|
|
41
39
|
} else {
|
|
42
|
-
|
|
43
|
-
return connStr;
|
|
40
|
+
return this.config.connectionString || "";
|
|
44
41
|
}
|
|
45
42
|
};
|
|
46
43
|
|
|
@@ -51,7 +48,6 @@ module.exports = function (RED) {
|
|
|
51
48
|
try {
|
|
52
49
|
const finalConnectionString = this._buildConnectionString();
|
|
53
50
|
if (!finalConnectionString) throw new Error("La chaîne de connexion est vide.");
|
|
54
|
-
|
|
55
51
|
const poolParams = {
|
|
56
52
|
connectionString: finalConnectionString,
|
|
57
53
|
initialSize: parseInt(this.config.initialSize, 10) || undefined,
|
|
@@ -61,9 +57,7 @@ module.exports = function (RED) {
|
|
|
61
57
|
connectionTimeout: (parseInt(this.config.connectionTimeout, 10) * 1000) || undefined,
|
|
62
58
|
loginTimeout: parseInt(this.config.loginTimeout, 10) || undefined
|
|
63
59
|
};
|
|
64
|
-
|
|
65
60
|
Object.keys(poolParams).forEach(key => poolParams[key] === undefined && delete poolParams[key]);
|
|
66
|
-
|
|
67
61
|
this.pool = await odbcModule.pool(poolParams);
|
|
68
62
|
this.connecting = false;
|
|
69
63
|
this.status({ fill: "green", shape: "dot", text: "Pool ready" });
|
|
@@ -100,9 +94,7 @@ module.exports = function (RED) {
|
|
|
100
94
|
try {
|
|
101
95
|
await Promise.race([
|
|
102
96
|
this.pool.close(),
|
|
103
|
-
new Promise((_, reject) =>
|
|
104
|
-
setTimeout(() => reject(new Error('Pool close timeout')), this.closeOperationTimeout)
|
|
105
|
-
)
|
|
97
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Pool close timeout')), this.closeOperationTimeout))
|
|
106
98
|
]);
|
|
107
99
|
this.log("Connection pool closed successfully for reset.");
|
|
108
100
|
closedSuccessfully = true;
|
|
@@ -111,11 +103,7 @@ module.exports = function (RED) {
|
|
|
111
103
|
} finally {
|
|
112
104
|
this.pool = null;
|
|
113
105
|
this.connecting = false;
|
|
114
|
-
|
|
115
|
-
this.status({ fill: "grey", shape: "ring", text: "Pool reset" });
|
|
116
|
-
} else {
|
|
117
|
-
this.status({ fill: "red", shape: "ring", text: "Pool reset failed" });
|
|
118
|
-
}
|
|
106
|
+
this.status({ fill: closedSuccessfully ? "grey" : "red", shape: "ring", text: closedSuccessfully ? "Pool reset" : "Pool reset failed" });
|
|
119
107
|
}
|
|
120
108
|
} else {
|
|
121
109
|
this.log("Pool reset requested, but no active pool to reset.");
|
|
@@ -128,9 +116,7 @@ module.exports = function (RED) {
|
|
|
128
116
|
try {
|
|
129
117
|
await Promise.race([
|
|
130
118
|
this.pool.close(),
|
|
131
|
-
new Promise((_, reject) =>
|
|
132
|
-
setTimeout(() => reject(new Error('Pool close timeout on node close')), this.closeOperationTimeout)
|
|
133
|
-
)
|
|
119
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Pool close timeout on node close')), this.closeOperationTimeout))
|
|
134
120
|
]);
|
|
135
121
|
this.log("Connection pool closed successfully on node close.");
|
|
136
122
|
} catch (error) {
|
|
@@ -142,21 +128,13 @@ module.exports = function (RED) {
|
|
|
142
128
|
done();
|
|
143
129
|
});
|
|
144
130
|
}
|
|
145
|
-
|
|
146
|
-
RED.nodes.registerType("odbc config", poolConfig, {
|
|
147
|
-
credentials: {
|
|
148
|
-
password: { type: "password" }
|
|
149
|
-
}
|
|
150
|
-
});
|
|
131
|
+
RED.nodes.registerType("odbc config", poolConfig, { credentials: { password: { type: "password" } } });
|
|
151
132
|
|
|
152
133
|
RED.httpAdmin.post("/odbc_config/:id/test", RED.auth.needsPermission("odbc.write"), async function(req, res) {
|
|
153
134
|
const tempConfig = req.body;
|
|
154
|
-
|
|
155
135
|
const buildTestConnectionString = () => {
|
|
156
136
|
if (tempConfig.connectionMode === 'structured') {
|
|
157
|
-
if (!tempConfig.dbType || !tempConfig.server) {
|
|
158
|
-
throw new Error("En mode structuré, le type de base de données et le serveur sont requis.");
|
|
159
|
-
}
|
|
137
|
+
if (!tempConfig.dbType || !tempConfig.server) { throw new Error("En mode structuré, le type de base de données et le serveur sont requis."); }
|
|
160
138
|
let driver;
|
|
161
139
|
let parts = [];
|
|
162
140
|
switch (tempConfig.dbType) {
|
|
@@ -177,25 +155,16 @@ module.exports = function (RED) {
|
|
|
177
155
|
return connStr;
|
|
178
156
|
}
|
|
179
157
|
};
|
|
180
|
-
|
|
181
158
|
let connection;
|
|
182
159
|
try {
|
|
183
160
|
const testConnectionString = buildTestConnectionString();
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const connectionOptions = {
|
|
187
|
-
connectionString: testConnectionString,
|
|
188
|
-
loginTimeout: 10
|
|
189
|
-
};
|
|
161
|
+
const connectionOptions = { connectionString: testConnectionString, loginTimeout: 10 };
|
|
190
162
|
connection = await odbcModule.connect(connectionOptions);
|
|
191
163
|
res.sendStatus(200);
|
|
192
164
|
} catch (err) {
|
|
193
|
-
// console.error("[ODBC Test] Connection failed:", err); // Conservé pour debug si besoin
|
|
194
165
|
res.status(500).send(err.message || "Erreur inconnue durant le test.");
|
|
195
166
|
} finally {
|
|
196
|
-
if (connection)
|
|
197
|
-
await connection.close();
|
|
198
|
-
}
|
|
167
|
+
if (connection) await connection.close();
|
|
199
168
|
}
|
|
200
169
|
});
|
|
201
170
|
|
|
@@ -208,14 +177,18 @@ module.exports = function (RED) {
|
|
|
208
177
|
this.isAwaitingRetry = false;
|
|
209
178
|
this.retryTimer = null;
|
|
210
179
|
this.cursorCloseOperationTimeout = 5000;
|
|
180
|
+
this.currentQueryForErrorContext = null;
|
|
181
|
+
this.currentParamsForErrorContext = null;
|
|
211
182
|
|
|
212
183
|
this.enhanceError = (error, query, params, defaultMessage = "Query error") => {
|
|
184
|
+
const q = query || this.currentQueryForErrorContext;
|
|
185
|
+
const p = params || this.currentParamsForErrorContext;
|
|
213
186
|
const queryContext = (() => {
|
|
214
187
|
let s = "";
|
|
215
|
-
if (
|
|
188
|
+
if (q || p) {
|
|
216
189
|
s += " {";
|
|
217
|
-
if (
|
|
218
|
-
if (
|
|
190
|
+
if (q) s += `"query": '${String(q).substring(0, 100)}${String(q).length > 100 ? "..." : ""}'`;
|
|
191
|
+
if (p) s += `, "params": '${JSON.stringify(p)}'`;
|
|
219
192
|
s += "}";
|
|
220
193
|
return s;
|
|
221
194
|
}
|
|
@@ -226,67 +199,48 @@ module.exports = function (RED) {
|
|
|
226
199
|
else if (typeof error === "string") { finalError = new Error(error); }
|
|
227
200
|
else { finalError = new Error(defaultMessage); }
|
|
228
201
|
finalError.message = `${finalError.message}${queryContext}`;
|
|
229
|
-
if (
|
|
230
|
-
if (
|
|
202
|
+
if (q) finalError.query = String(q).substring(0,200);
|
|
203
|
+
if (p) finalError.params = p;
|
|
231
204
|
return finalError;
|
|
232
205
|
};
|
|
233
206
|
|
|
234
|
-
// MODIFIÉ : Nettoyage des objets de résultat
|
|
235
207
|
this.executeQueryAndProcess = async (dbConnection, queryString, queryParams, msg) => {
|
|
236
208
|
const result = await dbConnection.query(queryString, queryParams);
|
|
237
209
|
if (typeof result === "undefined") { throw new Error("Query returned undefined."); }
|
|
238
|
-
|
|
239
210
|
const newMsg = RED.util.cloneMessage(msg);
|
|
240
|
-
const outputProperty = this.config.outputObj || "payload";
|
|
211
|
+
const outputProperty = this.config.outputObj || "payload";
|
|
241
212
|
const otherParams = {};
|
|
242
213
|
let actualDataRows = [];
|
|
243
|
-
|
|
244
214
|
if (result !== null && typeof result === "object") {
|
|
245
215
|
if (Array.isArray(result)) {
|
|
246
|
-
actualDataRows = result.map(row => {
|
|
247
|
-
if (typeof row === 'object' && row !== null) {
|
|
248
|
-
return { ...row }; // Copie superficielle pour "nettoyer" l'objet
|
|
249
|
-
}
|
|
250
|
-
return row;
|
|
251
|
-
});
|
|
252
|
-
|
|
216
|
+
actualDataRows = result.map(row => (typeof row === 'object' && row !== null) ? { ...row } : row);
|
|
253
217
|
for (const [key, value] of Object.entries(result)) {
|
|
254
|
-
if (isNaN(parseInt(key))) {
|
|
255
|
-
otherParams[key] = value;
|
|
256
|
-
}
|
|
218
|
+
if (isNaN(parseInt(key))) { otherParams[key] = value; }
|
|
257
219
|
}
|
|
258
220
|
} else {
|
|
259
|
-
for (const [key, value] of Object.entries(result)) {
|
|
260
|
-
otherParams[key] = value;
|
|
261
|
-
}
|
|
221
|
+
for (const [key, value] of Object.entries(result)) { otherParams[key] = value; }
|
|
262
222
|
}
|
|
263
223
|
}
|
|
264
|
-
|
|
265
224
|
const columnMetadata = otherParams.columns;
|
|
266
225
|
if (Array.isArray(columnMetadata) && Array.isArray(actualDataRows) && actualDataRows.length > 0) {
|
|
267
226
|
const sqlBitColumnNames = new Set();
|
|
268
|
-
columnMetadata.forEach((col)
|
|
269
|
-
if (col && typeof col.name === "string" && col.dataTypeName === "SQL_BIT") {
|
|
270
|
-
sqlBitColumnNames.add(col.name);
|
|
271
|
-
}
|
|
272
|
-
});
|
|
227
|
+
columnMetadata.forEach(col => { if (col && typeof col.name === "string" && col.dataTypeName === "SQL_BIT") sqlBitColumnNames.add(col.name); });
|
|
273
228
|
if (sqlBitColumnNames.size > 0) {
|
|
274
|
-
actualDataRows.forEach(
|
|
229
|
+
actualDataRows.forEach(row => {
|
|
275
230
|
if (typeof row === "object" && row !== null) {
|
|
276
231
|
for (const columnName of sqlBitColumnNames) {
|
|
277
232
|
if (row.hasOwnProperty(columnName)) {
|
|
278
233
|
const value = row[columnName];
|
|
279
|
-
if (value === "1" || value === 1)
|
|
280
|
-
else if (value === "0" || value === 0)
|
|
234
|
+
if (value === "1" || value === 1) row[columnName] = true;
|
|
235
|
+
else if (value === "0" || value === 0) row[columnName] = false;
|
|
281
236
|
}
|
|
282
237
|
}
|
|
283
238
|
}
|
|
284
239
|
});
|
|
285
240
|
}
|
|
286
241
|
}
|
|
287
|
-
|
|
288
242
|
objPath.set(newMsg, outputProperty, actualDataRows);
|
|
289
|
-
if (Object.keys(otherParams).length)
|
|
243
|
+
if (Object.keys(otherParams).length) newMsg.odbc = otherParams;
|
|
290
244
|
return newMsg;
|
|
291
245
|
};
|
|
292
246
|
|
|
@@ -294,21 +248,16 @@ module.exports = function (RED) {
|
|
|
294
248
|
const chunkSize = parseInt(this.config.streamChunkSize) || 1;
|
|
295
249
|
const fetchSize = chunkSize > 100 ? 100 : chunkSize;
|
|
296
250
|
let cursor;
|
|
297
|
-
|
|
298
251
|
try {
|
|
299
252
|
cursor = await dbConnection.query(queryString, queryParams, { cursor: true, fetchSize: fetchSize });
|
|
300
253
|
this.status({ fill: "blue", shape: "dot", text: "streaming rows..." });
|
|
301
|
-
|
|
302
254
|
let rowCount = 0;
|
|
303
255
|
let chunk = [];
|
|
304
|
-
|
|
305
256
|
while (true) {
|
|
306
257
|
const rows = await cursor.fetch();
|
|
307
|
-
if (!rows || rows.length === 0)
|
|
308
|
-
|
|
258
|
+
if (!rows || rows.length === 0) break;
|
|
309
259
|
for (const row of rows) {
|
|
310
260
|
rowCount++;
|
|
311
|
-
// Nettoyer chaque ligne aussi pour le streaming
|
|
312
261
|
const cleanRow = (typeof row === 'object' && row !== null) ? { ...row } : row;
|
|
313
262
|
chunk.push(cleanRow);
|
|
314
263
|
if (chunk.length >= chunkSize) {
|
|
@@ -320,48 +269,103 @@ module.exports = function (RED) {
|
|
|
320
269
|
}
|
|
321
270
|
}
|
|
322
271
|
}
|
|
323
|
-
|
|
324
272
|
if (chunk.length > 0) {
|
|
325
273
|
const newMsg = RED.util.cloneMessage(msg);
|
|
326
274
|
objPath.set(newMsg, this.config.outputObj || "payload", chunk);
|
|
327
275
|
newMsg.odbc_stream = { index: rowCount - chunk.length, count: chunk.length, complete: false };
|
|
328
276
|
send(newMsg);
|
|
329
277
|
}
|
|
330
|
-
|
|
331
278
|
const finalMsg = RED.util.cloneMessage(msg);
|
|
332
279
|
objPath.set(finalMsg, this.config.outputObj || "payload", []);
|
|
333
280
|
finalMsg.odbc_stream = { index: rowCount, count: 0, complete: true };
|
|
334
281
|
send(finalMsg);
|
|
335
|
-
|
|
336
282
|
this.status({ fill: "green", shape: "dot", text: `success (${rowCount} rows)` });
|
|
337
|
-
|
|
338
283
|
} finally {
|
|
339
284
|
if (cursor) {
|
|
340
285
|
try {
|
|
341
286
|
await Promise.race([
|
|
342
287
|
cursor.close(),
|
|
343
|
-
new Promise((_, reject) =>
|
|
344
|
-
setTimeout(() => reject(new Error('Cursor close timeout')), this.cursorCloseOperationTimeout)
|
|
345
|
-
)
|
|
288
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Cursor close timeout')), this.cursorCloseOperationTimeout))
|
|
346
289
|
]);
|
|
347
|
-
} catch (cursorCloseError) {
|
|
348
|
-
|
|
349
|
-
|
|
290
|
+
} catch (cursorCloseError) { this.warn(`Error or timeout closing cursor: ${cursorCloseError.message}`); }
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
this.testBasicConnectivity = async function(connection) {
|
|
296
|
+
if (!connection || typeof connection.query !== 'function') {
|
|
297
|
+
this.warn("Test de connectivité basique : connexion invalide fournie.");
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
let originalTimeout;
|
|
301
|
+
try {
|
|
302
|
+
originalTimeout = connection.queryTimeout;
|
|
303
|
+
connection.queryTimeout = 5;
|
|
304
|
+
await connection.query("SELECT 1");
|
|
305
|
+
this.log("Test de connectivité basique (SELECT 1) : Réussi.");
|
|
306
|
+
return true;
|
|
307
|
+
} catch (testError) {
|
|
308
|
+
this.warn(`Test de connectivité basique (SELECT 1) : Échoué - ${testError.message}`);
|
|
309
|
+
return false;
|
|
310
|
+
} finally {
|
|
311
|
+
if (typeof originalTimeout !== 'undefined' && connection && typeof connection.query === 'function') {
|
|
312
|
+
try { connection.queryTimeout = originalTimeout; }
|
|
313
|
+
catch(e) { this.warn("Impossible de restaurer le queryTimeout original après le test de connectivité.")}
|
|
350
314
|
}
|
|
351
315
|
}
|
|
352
316
|
};
|
|
353
317
|
|
|
318
|
+
this.getRenderedQueryAndParams = async function(msg) {
|
|
319
|
+
const querySourceType = this.config.querySourceType || 'msg';
|
|
320
|
+
const querySource = this.config.querySource || 'query';
|
|
321
|
+
const paramsSourceType = this.config.paramsSourceType || 'msg';
|
|
322
|
+
const paramsSource = this.config.paramsSource || 'parameters';
|
|
323
|
+
|
|
324
|
+
this.currentParamsForErrorContext = await new Promise(resolve => RED.util.evaluateNodeProperty(paramsSource, paramsSourceType, this, msg, (err, val) => resolve(err ? undefined : val)));
|
|
325
|
+
this.currentQueryForErrorContext = await new Promise(resolve => RED.util.evaluateNodeProperty(querySource, querySourceType, this, msg, (err, val) => resolve(err ? undefined : (val || this.config.query || ""))));
|
|
326
|
+
|
|
327
|
+
if (!this.currentQueryForErrorContext) {
|
|
328
|
+
throw new Error("No query to execute. Please provide a query in the node's configuration or via msg." + (querySourceType === 'msg' ? querySource : 'querySource (non-msg)'));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
let finalQuery = this.currentQueryForErrorContext;
|
|
332
|
+
const isPreparedStatement = this.currentParamsForErrorContext || (finalQuery && finalQuery.includes("?"));
|
|
333
|
+
if (!isPreparedStatement && finalQuery) {
|
|
334
|
+
finalQuery = mustache.render(finalQuery, msg);
|
|
335
|
+
}
|
|
336
|
+
return { query: finalQuery, params: this.currentParamsForErrorContext };
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
this.executeUserQuery = async function(connection, query, params, msg, send) {
|
|
340
|
+
const configuredTimeout = parseInt(this.poolNode.config.queryTimeoutSeconds, 10);
|
|
341
|
+
if (configuredTimeout > 0) {
|
|
342
|
+
try { connection.queryTimeout = configuredTimeout; }
|
|
343
|
+
catch (e) { this.warn(`Could not set queryTimeout on connection: ${e.message}`); }
|
|
344
|
+
} else {
|
|
345
|
+
connection.queryTimeout = 0;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
this.status({ fill: "blue", shape: "dot", text: "executing..." });
|
|
349
|
+
if (this.config.streaming) {
|
|
350
|
+
await this.executeStreamQuery(connection, query, params, msg, send);
|
|
351
|
+
} else {
|
|
352
|
+
const newMsg = await this.executeQueryAndProcess(connection, query, params, msg);
|
|
353
|
+
this.status({ fill: "green", shape: "dot", text: "success" });
|
|
354
|
+
send(newMsg);
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
|
|
354
358
|
this.on("input", async (msg, send, done) => {
|
|
359
|
+
this.currentQueryForErrorContext = null;
|
|
360
|
+
this.currentParamsForErrorContext = null;
|
|
361
|
+
|
|
355
362
|
if (this.isAwaitingRetry) {
|
|
356
363
|
if (this.poolNode && this.poolNode.config.retryOnMsg === true) {
|
|
357
364
|
this.log("New message received, overriding retry timer and attempting query now.");
|
|
358
|
-
clearTimeout(this.retryTimer);
|
|
359
|
-
this.retryTimer = null;
|
|
360
|
-
this.isAwaitingRetry = false;
|
|
365
|
+
clearTimeout(this.retryTimer); this.retryTimer = null; this.isAwaitingRetry = false;
|
|
361
366
|
} else {
|
|
362
|
-
this.warn("Node is in a retry-wait state. New message ignored
|
|
363
|
-
if (done) done();
|
|
364
|
-
return;
|
|
367
|
+
this.warn("Node is in a retry-wait state. New message ignored.");
|
|
368
|
+
if (done) done(); return;
|
|
365
369
|
}
|
|
366
370
|
}
|
|
367
371
|
this.isAwaitingRetry = false;
|
|
@@ -369,128 +373,130 @@ module.exports = function (RED) {
|
|
|
369
373
|
|
|
370
374
|
if (!this.poolNode) {
|
|
371
375
|
this.status({ fill: "red", shape: "ring", text: "No config node" });
|
|
372
|
-
return done(new Error("ODBC Config node not properly configured."));
|
|
376
|
+
return done(this.enhanceError(new Error("ODBC Config node not properly configured.")));
|
|
373
377
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
378
|
+
|
|
379
|
+
let queryToExecute;
|
|
380
|
+
let paramsToExecute;
|
|
381
|
+
try {
|
|
382
|
+
const queryData = await this.getRenderedQueryAndParams(msg);
|
|
383
|
+
queryToExecute = queryData.query;
|
|
384
|
+
paramsToExecute = queryData.params;
|
|
385
|
+
} catch (inputValidationError) {
|
|
386
|
+
this.status({ fill: "red", shape: "ring", text: "Input Error" });
|
|
387
|
+
return done(this.enhanceError(inputValidationError));
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
let activeConnection = null;
|
|
391
|
+
let errorForUser = null;
|
|
392
|
+
let shouldProceedToTimedRetry = false;
|
|
393
|
+
|
|
394
|
+
try {
|
|
395
|
+
this.status({ fill: "yellow", shape: "dot", text: "connecting (pool)..." });
|
|
396
|
+
activeConnection = await this.poolNode.connect();
|
|
397
|
+
await this.executeUserQuery(activeConnection, queryToExecute, paramsToExecute, msg, send);
|
|
398
|
+
|
|
399
|
+
done();
|
|
377
400
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
const paramsSource = this.config.paramsSource || 'parameters';
|
|
382
|
-
const params = await new Promise(resolve => RED.util.evaluateNodeProperty(paramsSource, paramsSourceType, this, msg, (err, val) => resolve(err ? undefined : val)));
|
|
383
|
-
let query = await new Promise(resolve => RED.util.evaluateNodeProperty(querySource, querySourceType, this, msg, (err, val) => resolve(err ? undefined : (val || this.config.query || ""))));
|
|
384
|
-
if (!query) throw new Error("No query to execute");
|
|
385
|
-
const isPreparedStatement = params || (query && query.includes("?"));
|
|
386
|
-
if (!isPreparedStatement && query) {
|
|
387
|
-
query = mustache.render(query, msg);
|
|
401
|
+
if (activeConnection) {
|
|
402
|
+
try { await activeConnection.close(); } catch(e) { this.warn("Error closing pool connection after success: " + e.message); }
|
|
403
|
+
activeConnection = null;
|
|
388
404
|
}
|
|
405
|
+
return;
|
|
389
406
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
407
|
+
} catch (initialDbError) {
|
|
408
|
+
this.warn(`Initial DB attempt failed: ${initialDbError.message}`);
|
|
409
|
+
// Garder la requête originale pour le contexte d'erreur, même si une erreur de connexion se produit
|
|
410
|
+
// this.currentQueryForErrorContext et this.currentParamsForErrorContext sont déjà settés par getRenderedQueryAndParams
|
|
411
|
+
|
|
412
|
+
if (activeConnection) {
|
|
413
|
+
const connStillGood = await this.testBasicConnectivity(activeConnection);
|
|
414
|
+
try { await activeConnection.close(); activeConnection = null; }
|
|
415
|
+
catch(e){ this.warn("Error closing pool conn after initial error: "+e.message); activeConnection = null; }
|
|
416
|
+
|
|
417
|
+
if (connStillGood) {
|
|
418
|
+
this.status({ fill: "red", shape: "ring", text: "SQL error" });
|
|
419
|
+
return done(this.enhanceError(initialDbError));
|
|
395
420
|
}
|
|
396
|
-
} else {
|
|
397
|
-
connection.queryTimeout = 0;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
this.status({ fill: "blue", shape: "dot", text: "executing..." });
|
|
401
|
-
if (this.config.streaming) {
|
|
402
|
-
await this.executeStreamQuery(connection, query, params, msg, send);
|
|
403
|
-
} else {
|
|
404
|
-
const newMsg = await this.executeQueryAndProcess(connection, query, params, msg);
|
|
405
|
-
this.status({ fill: "green", shape: "dot", text: "success" });
|
|
406
|
-
send(newMsg);
|
|
407
421
|
}
|
|
408
|
-
};
|
|
409
|
-
|
|
410
|
-
let connectionFromPool;
|
|
411
|
-
let errorAfterInitialAttempts = null;
|
|
412
|
-
|
|
413
|
-
try {
|
|
414
|
-
this.status({ fill: "yellow", shape: "dot", text: "connecting..." });
|
|
415
|
-
connectionFromPool = await this.poolNode.connect();
|
|
416
|
-
//this.log("[ODBC Node] DEBUG: Avant executeWithConnection (pooled)"); // Logs de debug
|
|
417
|
-
await executeWithConnection(connectionFromPool);
|
|
418
|
-
//this.log("[ODBC Node] DEBUG: Après executeWithConnection (pooled), avant fermeture et done()");
|
|
419
422
|
|
|
420
|
-
// Fermer la connexion avant d'appeler done() dans le chemin de succès principal
|
|
421
|
-
if (connectionFromPool) {
|
|
422
|
-
await connectionFromPool.close();
|
|
423
|
-
connectionFromPool = null;
|
|
424
|
-
}
|
|
425
|
-
return done();
|
|
426
|
-
} catch (poolError) {
|
|
427
|
-
if (connectionFromPool) {
|
|
428
|
-
try { await connectionFromPool.close(); } catch(e) { this.warn("Error closing pool connection in poolError catch: " + e.message); }
|
|
429
|
-
connectionFromPool = null;
|
|
430
|
-
}
|
|
431
|
-
this.warn(`First attempt with pooled connection failed: ${poolError.message}`);
|
|
432
|
-
|
|
433
423
|
if (this.poolNode.config.retryFreshConnection) {
|
|
434
424
|
this.warn("Attempting retry with a fresh connection.");
|
|
435
425
|
this.status({ fill: "yellow", shape: "dot", text: "Retrying (fresh)..." });
|
|
436
|
-
let freshConnection;
|
|
437
426
|
try {
|
|
438
427
|
const freshConnectConfig = this.poolNode.getFreshConnectionConfig();
|
|
439
|
-
|
|
440
|
-
this.log("Fresh connection established
|
|
441
|
-
|
|
442
|
-
await
|
|
443
|
-
|
|
444
|
-
|
|
428
|
+
activeConnection = await odbcModule.connect(freshConnectConfig);
|
|
429
|
+
this.log("Fresh connection established.");
|
|
430
|
+
|
|
431
|
+
const freshConnGood = await this.testBasicConnectivity(activeConnection);
|
|
432
|
+
if (!freshConnGood) {
|
|
433
|
+
errorForUser = this.enhanceError(new Error("Basic connectivity (SELECT 1) failed on fresh connection."), null, null, "Fresh Connection Test Failed");
|
|
434
|
+
shouldProceedToTimedRetry = true;
|
|
435
|
+
throw errorForUser;
|
|
436
|
+
}
|
|
445
437
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
438
|
+
await this.executeUserQuery(activeConnection, queryToExecute, paramsToExecute, msg, send);
|
|
439
|
+
|
|
440
|
+
this.log("Query successful with fresh connection. Resetting pool.");
|
|
441
|
+
done();
|
|
442
|
+
|
|
443
|
+
await this.poolNode.resetPool();
|
|
444
|
+
if (activeConnection) {
|
|
445
|
+
try { await activeConnection.close(); } catch(e) { this.warn("Error closing fresh connection after success: " + e.message); }
|
|
446
|
+
activeConnection = null;
|
|
449
447
|
}
|
|
450
|
-
return
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
448
|
+
return;
|
|
449
|
+
|
|
450
|
+
} catch (freshErrorOrConnectivityFail) {
|
|
451
|
+
if (activeConnection) { try { await activeConnection.close(); activeConnection = null; } catch(e){this.warn("Error closing fresh conn after error: "+e.message);} }
|
|
452
|
+
|
|
453
|
+
if (shouldProceedToTimedRetry) {
|
|
454
|
+
// errorForUser a été setté par l'échec du SELECT 1 sur la connexion fraîche
|
|
455
|
+
} else {
|
|
456
|
+
this.status({ fill: "red", shape: "ring", text: "SQL error (on retry)" });
|
|
457
|
+
return done(this.enhanceError(freshErrorOrConnectivityFail));
|
|
456
458
|
}
|
|
457
459
|
}
|
|
458
|
-
} else {
|
|
459
|
-
|
|
460
|
+
} else {
|
|
461
|
+
errorForUser = this.enhanceError(initialDbError, null, null, "Connection Error (no fresh retry)");
|
|
462
|
+
shouldProceedToTimedRetry = true;
|
|
460
463
|
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (activeConnection) { // Sécurité supplémentaire pour fermer une connexion si elle est restée active
|
|
467
|
+
try { await activeConnection.close(); } catch(e) { this.warn("Final cleanup: Error closing activeConnection: " + e.message); }
|
|
468
|
+
activeConnection = null;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (shouldProceedToTimedRetry && errorForUser) {
|
|
465
472
|
const retryDelaySeconds = parseInt(this.poolNode.config.retryDelay, 10);
|
|
466
473
|
if (retryDelaySeconds > 0) {
|
|
467
|
-
this.warn(`
|
|
474
|
+
this.warn(`Connection issue. Scheduling retry in ${retryDelaySeconds}s. Error: ${errorForUser.message}`);
|
|
468
475
|
this.status({ fill: "red", shape: "ring", text: `Retry in ${retryDelaySeconds}s...` });
|
|
469
476
|
this.isAwaitingRetry = true;
|
|
470
477
|
this.retryTimer = setTimeout(() => {
|
|
471
|
-
this.isAwaitingRetry = false;
|
|
472
|
-
this.
|
|
473
|
-
this.log(`Retry timer expired for message. Re-emitting for node ${this.id || this.name}.`);
|
|
478
|
+
this.isAwaitingRetry = false; this.retryTimer = null;
|
|
479
|
+
this.log(`Retry timer expired. Re-emitting message for node ${this.id || this.name}.`);
|
|
474
480
|
this.receive(msg);
|
|
475
481
|
}, retryDelaySeconds * 1000);
|
|
476
|
-
|
|
477
|
-
} else {
|
|
478
|
-
this.status({ fill: "red", shape: "ring", text: "
|
|
479
|
-
|
|
482
|
+
return done();
|
|
483
|
+
} else {
|
|
484
|
+
this.status({ fill: "red", shape: "ring", text: "Connection Error" });
|
|
485
|
+
return done(errorForUser);
|
|
480
486
|
}
|
|
487
|
+
} else if (errorForUser) {
|
|
488
|
+
this.status({ fill: "red", shape: "ring", text: "Error (No Timed Retry)" });
|
|
489
|
+
return done(errorForUser); // Cas où c'est une erreur SQL identifiée, pas de retry temporisé.
|
|
481
490
|
} else {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
if (done) return done();
|
|
491
|
+
// Ce chemin ne devrait pas être atteint si done() a été appelé dans un chemin de succès.
|
|
492
|
+
this.log("[ODBC Node] DEBUG: Reached unexpected end of on('input') path. Calling done().");
|
|
493
|
+
return done();
|
|
486
494
|
}
|
|
487
495
|
});
|
|
488
496
|
|
|
489
497
|
this.on("close", (done) => {
|
|
490
498
|
if (this.retryTimer) {
|
|
491
|
-
clearTimeout(this.retryTimer);
|
|
492
|
-
this.retryTimer = null;
|
|
493
|
-
this.isAwaitingRetry = false;
|
|
499
|
+
clearTimeout(this.retryTimer); this.retryTimer = null; this.isAwaitingRetry = false;
|
|
494
500
|
this.log("Cleared pending retry timer on node close/redeploy.");
|
|
495
501
|
}
|
|
496
502
|
this.status({});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bkmj/node-red-contrib-odbcmj",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.1",
|
|
4
4
|
"description": "A powerful Node-RED node to connect to any ODBC data source, with connection pooling, advanced retry logic, and result streaming.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"node-red",
|