@bkmj/node-red-contrib-odbcmj 2.4.0 → 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 +235 -106
- package/package.json +1 -1
package/odbc.js
CHANGED
|
@@ -15,7 +15,7 @@ module.exports = function (RED) {
|
|
|
15
15
|
if (isNaN(this.config.queryTimeoutSeconds) || this.config.queryTimeoutSeconds < 0) {
|
|
16
16
|
this.config.queryTimeoutSeconds = 0;
|
|
17
17
|
}
|
|
18
|
-
this.closeOperationTimeout = 10000;
|
|
18
|
+
this.closeOperationTimeout = 10000; // 10 secondes
|
|
19
19
|
|
|
20
20
|
this._buildConnectionString = function() {
|
|
21
21
|
if (this.config.connectionMode === 'structured') {
|
|
@@ -131,12 +131,33 @@ module.exports = function (RED) {
|
|
|
131
131
|
RED.nodes.registerType("odbc config", poolConfig, { credentials: { password: { type: "password" } } });
|
|
132
132
|
|
|
133
133
|
RED.httpAdmin.post("/odbc_config/:id/test", RED.auth.needsPermission("odbc.write"), async function(req, res) {
|
|
134
|
-
// ... (Logique du testeur de connexion - INCHANGÉE par rapport à votre dernière version)
|
|
135
134
|
const tempConfig = req.body;
|
|
136
|
-
const buildTestConnectionString = () => {
|
|
135
|
+
const buildTestConnectionString = () => {
|
|
136
|
+
if (tempConfig.connectionMode === 'structured') {
|
|
137
|
+
if (!tempConfig.dbType || !tempConfig.server) { throw new Error("En mode structuré, le type de base de données et le serveur sont requis."); }
|
|
138
|
+
let driver;
|
|
139
|
+
let parts = [];
|
|
140
|
+
switch (tempConfig.dbType) {
|
|
141
|
+
case 'sqlserver': driver = 'ODBC Driver 17 for SQL Server'; break;
|
|
142
|
+
case 'postgresql': driver = 'PostgreSQL Unicode'; break;
|
|
143
|
+
case 'mysql': driver = 'MySQL ODBC 8.0 Unicode Driver'; break;
|
|
144
|
+
default: driver = tempConfig.driver || ''; break;
|
|
145
|
+
}
|
|
146
|
+
if(driver) parts.unshift(`DRIVER={${driver}}`);
|
|
147
|
+
parts.push(`SERVER=${tempConfig.server}`);
|
|
148
|
+
if (tempConfig.database) parts.push(`DATABASE=${tempConfig.database}`);
|
|
149
|
+
if (tempConfig.user) parts.push(`UID=${tempConfig.user}`);
|
|
150
|
+
if (tempConfig.password) parts.push(`PWD=${tempConfig.password}`);
|
|
151
|
+
return parts.join(';');
|
|
152
|
+
} else {
|
|
153
|
+
let connStr = tempConfig.connectionString || "";
|
|
154
|
+
if (!connStr) { throw new Error("La chaîne de connexion ne peut pas être vide."); }
|
|
155
|
+
return connStr;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
137
158
|
let connection;
|
|
138
159
|
try {
|
|
139
|
-
const testConnectionString = buildTestConnectionString();
|
|
160
|
+
const testConnectionString = buildTestConnectionString();
|
|
140
161
|
const connectionOptions = { connectionString: testConnectionString, loginTimeout: 10 };
|
|
141
162
|
connection = await odbcModule.connect(connectionOptions);
|
|
142
163
|
res.sendStatus(200);
|
|
@@ -156,12 +177,10 @@ module.exports = function (RED) {
|
|
|
156
177
|
this.isAwaitingRetry = false;
|
|
157
178
|
this.retryTimer = null;
|
|
158
179
|
this.cursorCloseOperationTimeout = 5000;
|
|
159
|
-
this.currentQueryForErrorContext = null;
|
|
160
|
-
this.currentParamsForErrorContext = null;
|
|
161
|
-
|
|
180
|
+
this.currentQueryForErrorContext = null;
|
|
181
|
+
this.currentParamsForErrorContext = null;
|
|
162
182
|
|
|
163
183
|
this.enhanceError = (error, query, params, defaultMessage = "Query error") => {
|
|
164
|
-
// Utilise this.currentQueryForErrorContext et this.currentParamsForErrorContext s'ils sont définis
|
|
165
184
|
const q = query || this.currentQueryForErrorContext;
|
|
166
185
|
const p = params || this.currentParamsForErrorContext;
|
|
167
186
|
const queryContext = (() => {
|
|
@@ -185,27 +204,156 @@ module.exports = function (RED) {
|
|
|
185
204
|
return finalError;
|
|
186
205
|
};
|
|
187
206
|
|
|
188
|
-
this.executeQueryAndProcess = async (dbConnection, queryString, queryParams, msg) => {
|
|
189
|
-
|
|
207
|
+
this.executeQueryAndProcess = async (dbConnection, queryString, queryParams, msg) => {
|
|
208
|
+
const result = await dbConnection.query(queryString, queryParams);
|
|
209
|
+
if (typeof result === "undefined") { throw new Error("Query returned undefined."); }
|
|
210
|
+
const newMsg = RED.util.cloneMessage(msg);
|
|
211
|
+
const outputProperty = this.config.outputObj || "payload";
|
|
212
|
+
const otherParams = {};
|
|
213
|
+
let actualDataRows = [];
|
|
214
|
+
if (result !== null && typeof result === "object") {
|
|
215
|
+
if (Array.isArray(result)) {
|
|
216
|
+
actualDataRows = result.map(row => (typeof row === 'object' && row !== null) ? { ...row } : row);
|
|
217
|
+
for (const [key, value] of Object.entries(result)) {
|
|
218
|
+
if (isNaN(parseInt(key))) { otherParams[key] = value; }
|
|
219
|
+
}
|
|
220
|
+
} else {
|
|
221
|
+
for (const [key, value] of Object.entries(result)) { otherParams[key] = value; }
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const columnMetadata = otherParams.columns;
|
|
225
|
+
if (Array.isArray(columnMetadata) && Array.isArray(actualDataRows) && actualDataRows.length > 0) {
|
|
226
|
+
const sqlBitColumnNames = new Set();
|
|
227
|
+
columnMetadata.forEach(col => { if (col && typeof col.name === "string" && col.dataTypeName === "SQL_BIT") sqlBitColumnNames.add(col.name); });
|
|
228
|
+
if (sqlBitColumnNames.size > 0) {
|
|
229
|
+
actualDataRows.forEach(row => {
|
|
230
|
+
if (typeof row === "object" && row !== null) {
|
|
231
|
+
for (const columnName of sqlBitColumnNames) {
|
|
232
|
+
if (row.hasOwnProperty(columnName)) {
|
|
233
|
+
const value = row[columnName];
|
|
234
|
+
if (value === "1" || value === 1) row[columnName] = true;
|
|
235
|
+
else if (value === "0" || value === 0) row[columnName] = false;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
objPath.set(newMsg, outputProperty, actualDataRows);
|
|
243
|
+
if (Object.keys(otherParams).length) newMsg.odbc = otherParams;
|
|
244
|
+
return newMsg;
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
this.executeStreamQuery = async (dbConnection, queryString, queryParams, msg, send) => {
|
|
248
|
+
const chunkSize = parseInt(this.config.streamChunkSize) || 1;
|
|
249
|
+
const fetchSize = chunkSize > 100 ? 100 : chunkSize;
|
|
250
|
+
let cursor;
|
|
251
|
+
try {
|
|
252
|
+
cursor = await dbConnection.query(queryString, queryParams, { cursor: true, fetchSize: fetchSize });
|
|
253
|
+
this.status({ fill: "blue", shape: "dot", text: "streaming rows..." });
|
|
254
|
+
let rowCount = 0;
|
|
255
|
+
let chunk = [];
|
|
256
|
+
while (true) {
|
|
257
|
+
const rows = await cursor.fetch();
|
|
258
|
+
if (!rows || rows.length === 0) break;
|
|
259
|
+
for (const row of rows) {
|
|
260
|
+
rowCount++;
|
|
261
|
+
const cleanRow = (typeof row === 'object' && row !== null) ? { ...row } : row;
|
|
262
|
+
chunk.push(cleanRow);
|
|
263
|
+
if (chunk.length >= chunkSize) {
|
|
264
|
+
const newMsg = RED.util.cloneMessage(msg);
|
|
265
|
+
objPath.set(newMsg, this.config.outputObj || "payload", chunk);
|
|
266
|
+
newMsg.odbc_stream = { index: rowCount - chunk.length, count: chunk.length, complete: false };
|
|
267
|
+
send(newMsg);
|
|
268
|
+
chunk = [];
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (chunk.length > 0) {
|
|
273
|
+
const newMsg = RED.util.cloneMessage(msg);
|
|
274
|
+
objPath.set(newMsg, this.config.outputObj || "payload", chunk);
|
|
275
|
+
newMsg.odbc_stream = { index: rowCount - chunk.length, count: chunk.length, complete: false };
|
|
276
|
+
send(newMsg);
|
|
277
|
+
}
|
|
278
|
+
const finalMsg = RED.util.cloneMessage(msg);
|
|
279
|
+
objPath.set(finalMsg, this.config.outputObj || "payload", []);
|
|
280
|
+
finalMsg.odbc_stream = { index: rowCount, count: 0, complete: true };
|
|
281
|
+
send(finalMsg);
|
|
282
|
+
this.status({ fill: "green", shape: "dot", text: `success (${rowCount} rows)` });
|
|
283
|
+
} finally {
|
|
284
|
+
if (cursor) {
|
|
285
|
+
try {
|
|
286
|
+
await Promise.race([
|
|
287
|
+
cursor.close(),
|
|
288
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Cursor close timeout')), this.cursorCloseOperationTimeout))
|
|
289
|
+
]);
|
|
290
|
+
} catch (cursorCloseError) { this.warn(`Error or timeout closing cursor: ${cursorCloseError.message}`); }
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
};
|
|
190
294
|
|
|
191
|
-
|
|
192
|
-
async function testBasicConnectivity(connection, nodeInstance) {
|
|
295
|
+
this.testBasicConnectivity = async function(connection) {
|
|
193
296
|
if (!connection || typeof connection.query !== 'function') {
|
|
194
|
-
|
|
297
|
+
this.warn("Test de connectivité basique : connexion invalide fournie.");
|
|
195
298
|
return false;
|
|
196
299
|
}
|
|
300
|
+
let originalTimeout;
|
|
197
301
|
try {
|
|
198
|
-
|
|
199
|
-
connection.queryTimeout = 5;
|
|
200
|
-
await connection.query("SELECT 1");
|
|
201
|
-
|
|
202
|
-
nodeInstance.log("Test de connectivité basique (SELECT 1) : Réussi.");
|
|
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.");
|
|
203
306
|
return true;
|
|
204
307
|
} catch (testError) {
|
|
205
|
-
|
|
308
|
+
this.warn(`Test de connectivité basique (SELECT 1) : Échoué - ${testError.message}`);
|
|
206
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é.")}
|
|
314
|
+
}
|
|
207
315
|
}
|
|
208
|
-
}
|
|
316
|
+
};
|
|
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
|
+
};
|
|
209
357
|
|
|
210
358
|
this.on("input", async (msg, send, done) => {
|
|
211
359
|
this.currentQueryForErrorContext = null;
|
|
@@ -225,75 +373,53 @@ module.exports = function (RED) {
|
|
|
225
373
|
|
|
226
374
|
if (!this.poolNode) {
|
|
227
375
|
this.status({ fill: "red", shape: "ring", text: "No config node" });
|
|
228
|
-
return done(new Error("ODBC Config node not properly configured."));
|
|
376
|
+
return done(this.enhanceError(new Error("ODBC Config node not properly configured.")));
|
|
229
377
|
}
|
|
230
|
-
|
|
231
|
-
const getRenderedQueryAndParams = async () => {
|
|
232
|
-
const querySourceType = this.config.querySourceType || 'msg';
|
|
233
|
-
const querySource = this.config.querySource || 'query';
|
|
234
|
-
const paramsSourceType = this.config.paramsSourceType || 'msg';
|
|
235
|
-
const paramsSource = this.config.paramsSource || 'parameters';
|
|
236
|
-
|
|
237
|
-
this.currentParamsForErrorContext = await new Promise(resolve => RED.util.evaluateNodeProperty(paramsSource, paramsSourceType, this, msg, (err, val) => resolve(err ? undefined : val)));
|
|
238
|
-
this.currentQueryForErrorContext = await new Promise(resolve => RED.util.evaluateNodeProperty(querySource, querySourceType, this, msg, (err, val) => resolve(err ? undefined : (val || this.config.query || ""))));
|
|
239
|
-
|
|
240
|
-
if (!this.currentQueryForErrorContext) throw new Error("No query to execute");
|
|
241
|
-
|
|
242
|
-
let finalQuery = this.currentQueryForErrorContext;
|
|
243
|
-
const isPreparedStatement = this.currentParamsForErrorContext || (finalQuery && finalQuery.includes("?"));
|
|
244
|
-
if (!isPreparedStatement && finalQuery) {
|
|
245
|
-
finalQuery = mustache.render(finalQuery, msg);
|
|
246
|
-
}
|
|
247
|
-
return { query: finalQuery, params: this.currentParamsForErrorContext };
|
|
248
|
-
};
|
|
249
378
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
} else {
|
|
264
|
-
const newMsg = await this.executeQueryAndProcess(connection, query, params, msg);
|
|
265
|
-
this.status({ fill: "green", shape: "dot", text: "success" });
|
|
266
|
-
send(newMsg);
|
|
267
|
-
}
|
|
268
|
-
};
|
|
269
|
-
|
|
270
|
-
let activeConnection = null; // Pour gérer la connexion active (pool ou fraîche)
|
|
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;
|
|
271
392
|
let shouldProceedToTimedRetry = false;
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
try { // Tentative Principale (avec connexion du pool)
|
|
275
|
-
const { query, params } = await getRenderedQueryAndParams();
|
|
276
|
-
|
|
393
|
+
|
|
394
|
+
try {
|
|
277
395
|
this.status({ fill: "yellow", shape: "dot", text: "connecting (pool)..." });
|
|
278
396
|
activeConnection = await this.poolNode.connect();
|
|
279
|
-
await executeUserQuery(activeConnection,
|
|
397
|
+
await this.executeUserQuery(activeConnection, queryToExecute, paramsToExecute, msg, send);
|
|
280
398
|
|
|
281
|
-
|
|
282
|
-
|
|
399
|
+
done();
|
|
400
|
+
|
|
401
|
+
if (activeConnection) {
|
|
402
|
+
try { await activeConnection.close(); } catch(e) { this.warn("Error closing pool connection after success: " + e.message); }
|
|
403
|
+
activeConnection = null;
|
|
404
|
+
}
|
|
405
|
+
return;
|
|
406
|
+
|
|
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
|
|
283
411
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
try { await activeConnection.close(); activeConnection = null; } catch(e){this.warn("Error closing pool conn after initial error: "+e.message);}
|
|
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; }
|
|
289
416
|
|
|
290
|
-
if (connStillGood) {
|
|
417
|
+
if (connStillGood) {
|
|
291
418
|
this.status({ fill: "red", shape: "ring", text: "SQL error" });
|
|
292
|
-
return done(this.enhanceError(
|
|
419
|
+
return done(this.enhanceError(initialDbError));
|
|
293
420
|
}
|
|
294
421
|
}
|
|
295
|
-
|
|
296
|
-
|
|
422
|
+
|
|
297
423
|
if (this.poolNode.config.retryFreshConnection) {
|
|
298
424
|
this.warn("Attempting retry with a fresh connection.");
|
|
299
425
|
this.status({ fill: "yellow", shape: "dot", text: "Retrying (fresh)..." });
|
|
@@ -302,47 +428,50 @@ module.exports = function (RED) {
|
|
|
302
428
|
activeConnection = await odbcModule.connect(freshConnectConfig);
|
|
303
429
|
this.log("Fresh connection established.");
|
|
304
430
|
|
|
305
|
-
const freshConnGood = await testBasicConnectivity(activeConnection
|
|
431
|
+
const freshConnGood = await this.testBasicConnectivity(activeConnection);
|
|
306
432
|
if (!freshConnGood) {
|
|
307
|
-
|
|
308
|
-
errorForTimedRetry = this.enhanceError(new Error("Basic connectivity (SELECT 1) failed on fresh connection."), this.currentQueryForErrorContext, this.currentParamsForErrorContext, "Fresh Connection Test Failed");
|
|
433
|
+
errorForUser = this.enhanceError(new Error("Basic connectivity (SELECT 1) failed on fresh connection."), null, null, "Fresh Connection Test Failed");
|
|
309
434
|
shouldProceedToTimedRetry = true;
|
|
310
|
-
throw
|
|
435
|
+
throw errorForUser;
|
|
311
436
|
}
|
|
312
437
|
|
|
313
|
-
|
|
314
|
-
const { query, params } = await getRenderedQueryAndParams(); // Re-préparer au cas où
|
|
315
|
-
await executeUserQuery(activeConnection, query, params);
|
|
438
|
+
await this.executeUserQuery(activeConnection, queryToExecute, paramsToExecute, msg, send);
|
|
316
439
|
|
|
317
440
|
this.log("Query successful with fresh connection. Resetting pool.");
|
|
441
|
+
done();
|
|
442
|
+
|
|
318
443
|
await this.poolNode.resetPool();
|
|
319
|
-
if (activeConnection) {
|
|
320
|
-
|
|
444
|
+
if (activeConnection) {
|
|
445
|
+
try { await activeConnection.close(); } catch(e) { this.warn("Error closing fresh connection after success: " + e.message); }
|
|
446
|
+
activeConnection = null;
|
|
447
|
+
}
|
|
448
|
+
return;
|
|
321
449
|
|
|
322
450
|
} catch (freshErrorOrConnectivityFail) {
|
|
323
|
-
// Soit odbcModule.connect a échoué, soit SELECT 1 a échoué (et errorForTimedRetry est déjà setté),
|
|
324
|
-
// soit executeUserQuery sur la connexion fraîche a échoué.
|
|
325
451
|
if (activeConnection) { try { await activeConnection.close(); activeConnection = null; } catch(e){this.warn("Error closing fresh conn after error: "+e.message);} }
|
|
326
452
|
|
|
327
|
-
if (shouldProceedToTimedRetry) {
|
|
328
|
-
//
|
|
453
|
+
if (shouldProceedToTimedRetry) {
|
|
454
|
+
// errorForUser a été setté par l'échec du SELECT 1 sur la connexion fraîche
|
|
329
455
|
} else {
|
|
330
|
-
// SELECT 1 sur connexion fraîche a réussi, mais la requête utilisateur a échoué. C'est une erreur SQL.
|
|
331
456
|
this.status({ fill: "red", shape: "ring", text: "SQL error (on retry)" });
|
|
332
|
-
return done(this.enhanceError(freshErrorOrConnectivityFail
|
|
457
|
+
return done(this.enhanceError(freshErrorOrConnectivityFail));
|
|
333
458
|
}
|
|
334
459
|
}
|
|
335
|
-
} else {
|
|
336
|
-
|
|
460
|
+
} else {
|
|
461
|
+
errorForUser = this.enhanceError(initialDbError, null, null, "Connection Error (no fresh retry)");
|
|
337
462
|
shouldProceedToTimedRetry = true;
|
|
338
463
|
}
|
|
339
464
|
}
|
|
340
465
|
|
|
341
|
-
//
|
|
342
|
-
|
|
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) {
|
|
343
472
|
const retryDelaySeconds = parseInt(this.poolNode.config.retryDelay, 10);
|
|
344
473
|
if (retryDelaySeconds > 0) {
|
|
345
|
-
this.warn(`Connection issue
|
|
474
|
+
this.warn(`Connection issue. Scheduling retry in ${retryDelaySeconds}s. Error: ${errorForUser.message}`);
|
|
346
475
|
this.status({ fill: "red", shape: "ring", text: `Retry in ${retryDelaySeconds}s...` });
|
|
347
476
|
this.isAwaitingRetry = true;
|
|
348
477
|
this.retryTimer = setTimeout(() => {
|
|
@@ -350,17 +479,17 @@ module.exports = function (RED) {
|
|
|
350
479
|
this.log(`Retry timer expired. Re-emitting message for node ${this.id || this.name}.`);
|
|
351
480
|
this.receive(msg);
|
|
352
481
|
}, retryDelaySeconds * 1000);
|
|
353
|
-
return done();
|
|
354
|
-
} else {
|
|
482
|
+
return done();
|
|
483
|
+
} else {
|
|
355
484
|
this.status({ fill: "red", shape: "ring", text: "Connection Error" });
|
|
356
|
-
return done(
|
|
485
|
+
return done(errorForUser);
|
|
357
486
|
}
|
|
358
|
-
} else if (
|
|
359
|
-
this.status({ fill: "red", shape: "ring", text: "Error (No Retry)" });
|
|
360
|
-
return done(
|
|
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é.
|
|
361
490
|
} else {
|
|
362
|
-
//
|
|
363
|
-
this.log("[ODBC Node] DEBUG: Reached end of on('input')
|
|
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().");
|
|
364
493
|
return done();
|
|
365
494
|
}
|
|
366
495
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bkmj/node-red-contrib-odbcmj",
|
|
3
|
-
"version": "2.4.
|
|
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",
|