@bkmj/node-red-contrib-odbcmj 2.3.1 → 2.4.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/odbc.js +137 -260
- package/package.json +1 -1
package/odbc.js
CHANGED
|
@@ -9,15 +9,13 @@ 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
|
-
this.closeOperationTimeout = 10000; // 10 secondes
|
|
18
|
+
this.closeOperationTimeout = 10000;
|
|
21
19
|
|
|
22
20
|
this._buildConnectionString = function() {
|
|
23
21
|
if (this.config.connectionMode === 'structured') {
|
|
@@ -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,60 +128,22 @@ 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) {
|
|
134
|
+
// ... (Logique du testeur de connexion - INCHANGÉE par rapport à votre dernière version)
|
|
153
135
|
const tempConfig = req.body;
|
|
154
|
-
|
|
155
|
-
const buildTestConnectionString = () => {
|
|
156
|
-
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
|
-
}
|
|
160
|
-
let driver;
|
|
161
|
-
let parts = [];
|
|
162
|
-
switch (tempConfig.dbType) {
|
|
163
|
-
case 'sqlserver': driver = 'ODBC Driver 17 for SQL Server'; break;
|
|
164
|
-
case 'postgresql': driver = 'PostgreSQL Unicode'; break;
|
|
165
|
-
case 'mysql': driver = 'MySQL ODBC 8.0 Unicode Driver'; break;
|
|
166
|
-
default: driver = tempConfig.driver || ''; break;
|
|
167
|
-
}
|
|
168
|
-
if(driver) parts.unshift(`DRIVER={${driver}}`);
|
|
169
|
-
parts.push(`SERVER=${tempConfig.server}`);
|
|
170
|
-
if (tempConfig.database) parts.push(`DATABASE=${tempConfig.database}`);
|
|
171
|
-
if (tempConfig.user) parts.push(`UID=${tempConfig.user}`);
|
|
172
|
-
if (tempConfig.password) parts.push(`PWD=${tempConfig.password}`);
|
|
173
|
-
return parts.join(';');
|
|
174
|
-
} else {
|
|
175
|
-
let connStr = tempConfig.connectionString || "";
|
|
176
|
-
if (!connStr) { throw new Error("La chaîne de connexion ne peut pas être vide."); }
|
|
177
|
-
return connStr;
|
|
178
|
-
}
|
|
179
|
-
};
|
|
180
|
-
|
|
136
|
+
const buildTestConnectionString = () => { /* ... */ }; // Définition interne
|
|
181
137
|
let connection;
|
|
182
138
|
try {
|
|
183
|
-
const testConnectionString = buildTestConnectionString();
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const connectionOptions = {
|
|
187
|
-
connectionString: testConnectionString,
|
|
188
|
-
loginTimeout: 10
|
|
189
|
-
};
|
|
139
|
+
const testConnectionString = buildTestConnectionString(); // Utilise la définition interne
|
|
140
|
+
const connectionOptions = { connectionString: testConnectionString, loginTimeout: 10 };
|
|
190
141
|
connection = await odbcModule.connect(connectionOptions);
|
|
191
142
|
res.sendStatus(200);
|
|
192
143
|
} catch (err) {
|
|
193
|
-
// console.error("[ODBC Test] Connection failed:", err); // Conservé pour debug si besoin
|
|
194
144
|
res.status(500).send(err.message || "Erreur inconnue durant le test.");
|
|
195
145
|
} finally {
|
|
196
|
-
if (connection)
|
|
197
|
-
await connection.close();
|
|
198
|
-
}
|
|
146
|
+
if (connection) await connection.close();
|
|
199
147
|
}
|
|
200
148
|
});
|
|
201
149
|
|
|
@@ -208,14 +156,20 @@ module.exports = function (RED) {
|
|
|
208
156
|
this.isAwaitingRetry = false;
|
|
209
157
|
this.retryTimer = null;
|
|
210
158
|
this.cursorCloseOperationTimeout = 5000;
|
|
159
|
+
this.currentQueryForErrorContext = null; // Pour stocker la requête lors du traitement
|
|
160
|
+
this.currentParamsForErrorContext = null; // Pour stocker les paramètres lors du traitement
|
|
161
|
+
|
|
211
162
|
|
|
212
163
|
this.enhanceError = (error, query, params, defaultMessage = "Query error") => {
|
|
164
|
+
// Utilise this.currentQueryForErrorContext et this.currentParamsForErrorContext s'ils sont définis
|
|
165
|
+
const q = query || this.currentQueryForErrorContext;
|
|
166
|
+
const p = params || this.currentParamsForErrorContext;
|
|
213
167
|
const queryContext = (() => {
|
|
214
168
|
let s = "";
|
|
215
|
-
if (
|
|
169
|
+
if (q || p) {
|
|
216
170
|
s += " {";
|
|
217
|
-
if (
|
|
218
|
-
if (
|
|
171
|
+
if (q) s += `"query": '${String(q).substring(0, 100)}${String(q).length > 100 ? "..." : ""}'`;
|
|
172
|
+
if (p) s += `, "params": '${JSON.stringify(p)}'`;
|
|
219
173
|
s += "}";
|
|
220
174
|
return s;
|
|
221
175
|
}
|
|
@@ -226,142 +180,44 @@ module.exports = function (RED) {
|
|
|
226
180
|
else if (typeof error === "string") { finalError = new Error(error); }
|
|
227
181
|
else { finalError = new Error(defaultMessage); }
|
|
228
182
|
finalError.message = `${finalError.message}${queryContext}`;
|
|
229
|
-
if (
|
|
230
|
-
if (
|
|
183
|
+
if (q) finalError.query = String(q).substring(0,200);
|
|
184
|
+
if (p) finalError.params = p;
|
|
231
185
|
return finalError;
|
|
232
186
|
};
|
|
233
187
|
|
|
234
|
-
|
|
235
|
-
this.
|
|
236
|
-
const result = await dbConnection.query(queryString, queryParams);
|
|
237
|
-
if (typeof result === "undefined") { throw new Error("Query returned undefined."); }
|
|
238
|
-
|
|
239
|
-
const newMsg = RED.util.cloneMessage(msg);
|
|
240
|
-
const outputProperty = this.config.outputObj || "payload"; // Utiliser la propriété configurée ou 'payload' par défaut
|
|
241
|
-
const otherParams = {};
|
|
242
|
-
let actualDataRows = [];
|
|
243
|
-
|
|
244
|
-
if (result !== null && typeof result === "object") {
|
|
245
|
-
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
|
-
});
|
|
188
|
+
this.executeQueryAndProcess = async (dbConnection, queryString, queryParams, msg) => { /* ... (inchangé) ... */ };
|
|
189
|
+
this.executeStreamQuery = async (dbConnection, queryString, queryParams, msg, send) => { /* ... (inchangé) ... */ };
|
|
252
190
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
} else {
|
|
259
|
-
for (const [key, value] of Object.entries(result)) {
|
|
260
|
-
otherParams[key] = value;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
191
|
+
// NOUVELLE fonction utilitaire
|
|
192
|
+
async function testBasicConnectivity(connection, nodeInstance) {
|
|
193
|
+
if (!connection || typeof connection.query !== 'function') {
|
|
194
|
+
nodeInstance.warn("Test de connectivité basique : connexion invalide fournie.");
|
|
195
|
+
return false;
|
|
263
196
|
}
|
|
264
|
-
|
|
265
|
-
const columnMetadata = otherParams.columns;
|
|
266
|
-
if (Array.isArray(columnMetadata) && Array.isArray(actualDataRows) && actualDataRows.length > 0) {
|
|
267
|
-
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
|
-
});
|
|
273
|
-
if (sqlBitColumnNames.size > 0) {
|
|
274
|
-
actualDataRows.forEach((row) => {
|
|
275
|
-
if (typeof row === "object" && row !== null) {
|
|
276
|
-
for (const columnName of sqlBitColumnNames) {
|
|
277
|
-
if (row.hasOwnProperty(columnName)) {
|
|
278
|
-
const value = row[columnName];
|
|
279
|
-
if (value === "1" || value === 1) { row[columnName] = true; }
|
|
280
|
-
else if (value === "0" || value === 0) { row[columnName] = false; }
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
objPath.set(newMsg, outputProperty, actualDataRows);
|
|
289
|
-
if (Object.keys(otherParams).length) { newMsg.odbc = otherParams; }
|
|
290
|
-
return newMsg;
|
|
291
|
-
};
|
|
292
|
-
|
|
293
|
-
this.executeStreamQuery = async (dbConnection, queryString, queryParams, msg, send) => {
|
|
294
|
-
const chunkSize = parseInt(this.config.streamChunkSize) || 1;
|
|
295
|
-
const fetchSize = chunkSize > 100 ? 100 : chunkSize;
|
|
296
|
-
let cursor;
|
|
297
|
-
|
|
298
197
|
try {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
for (const row of rows) {
|
|
310
|
-
rowCount++;
|
|
311
|
-
// Nettoyer chaque ligne aussi pour le streaming
|
|
312
|
-
const cleanRow = (typeof row === 'object' && row !== null) ? { ...row } : row;
|
|
313
|
-
chunk.push(cleanRow);
|
|
314
|
-
if (chunk.length >= chunkSize) {
|
|
315
|
-
const newMsg = RED.util.cloneMessage(msg);
|
|
316
|
-
objPath.set(newMsg, this.config.outputObj || "payload", chunk);
|
|
317
|
-
newMsg.odbc_stream = { index: rowCount - chunk.length, count: chunk.length, complete: false };
|
|
318
|
-
send(newMsg);
|
|
319
|
-
chunk = [];
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
if (chunk.length > 0) {
|
|
325
|
-
const newMsg = RED.util.cloneMessage(msg);
|
|
326
|
-
objPath.set(newMsg, this.config.outputObj || "payload", chunk);
|
|
327
|
-
newMsg.odbc_stream = { index: rowCount - chunk.length, count: chunk.length, complete: false };
|
|
328
|
-
send(newMsg);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
const finalMsg = RED.util.cloneMessage(msg);
|
|
332
|
-
objPath.set(finalMsg, this.config.outputObj || "payload", []);
|
|
333
|
-
finalMsg.odbc_stream = { index: rowCount, count: 0, complete: true };
|
|
334
|
-
send(finalMsg);
|
|
335
|
-
|
|
336
|
-
this.status({ fill: "green", shape: "dot", text: `success (${rowCount} rows)` });
|
|
337
|
-
|
|
338
|
-
} finally {
|
|
339
|
-
if (cursor) {
|
|
340
|
-
try {
|
|
341
|
-
await Promise.race([
|
|
342
|
-
cursor.close(),
|
|
343
|
-
new Promise((_, reject) =>
|
|
344
|
-
setTimeout(() => reject(new Error('Cursor close timeout')), this.cursorCloseOperationTimeout)
|
|
345
|
-
)
|
|
346
|
-
]);
|
|
347
|
-
} catch (cursorCloseError) {
|
|
348
|
-
this.warn(`Error or timeout closing cursor: ${cursorCloseError.message}`);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
198
|
+
const originalTimeout = connection.queryTimeout;
|
|
199
|
+
connection.queryTimeout = 5; // Court timeout pour un SELECT 1
|
|
200
|
+
await connection.query("SELECT 1"); // Ou équivalent SGBD
|
|
201
|
+
connection.queryTimeout = originalTimeout;
|
|
202
|
+
nodeInstance.log("Test de connectivité basique (SELECT 1) : Réussi.");
|
|
203
|
+
return true;
|
|
204
|
+
} catch (testError) {
|
|
205
|
+
nodeInstance.warn(`Test de connectivité basique (SELECT 1) : Échoué - ${testError.message}`);
|
|
206
|
+
return false;
|
|
351
207
|
}
|
|
352
|
-
}
|
|
208
|
+
}
|
|
353
209
|
|
|
354
210
|
this.on("input", async (msg, send, done) => {
|
|
211
|
+
this.currentQueryForErrorContext = null;
|
|
212
|
+
this.currentParamsForErrorContext = null;
|
|
213
|
+
|
|
355
214
|
if (this.isAwaitingRetry) {
|
|
356
215
|
if (this.poolNode && this.poolNode.config.retryOnMsg === true) {
|
|
357
216
|
this.log("New message received, overriding retry timer and attempting query now.");
|
|
358
|
-
clearTimeout(this.retryTimer);
|
|
359
|
-
this.retryTimer = null;
|
|
360
|
-
this.isAwaitingRetry = false;
|
|
217
|
+
clearTimeout(this.retryTimer); this.retryTimer = null; this.isAwaitingRetry = false;
|
|
361
218
|
} else {
|
|
362
|
-
this.warn("Node is in a retry-wait state. New message ignored
|
|
363
|
-
if (done) done();
|
|
364
|
-
return;
|
|
219
|
+
this.warn("Node is in a retry-wait state. New message ignored.");
|
|
220
|
+
if (done) done(); return;
|
|
365
221
|
}
|
|
366
222
|
}
|
|
367
223
|
this.isAwaitingRetry = false;
|
|
@@ -372,29 +228,33 @@ module.exports = function (RED) {
|
|
|
372
228
|
return done(new Error("ODBC Config node not properly configured."));
|
|
373
229
|
}
|
|
374
230
|
|
|
375
|
-
const
|
|
376
|
-
const outputProperty = this.config.outputObj || "payload"; // S'assurer que outputProperty est défini
|
|
377
|
-
|
|
231
|
+
const getRenderedQueryAndParams = async () => {
|
|
378
232
|
const querySourceType = this.config.querySourceType || 'msg';
|
|
379
233
|
const querySource = this.config.querySource || 'query';
|
|
380
234
|
const paramsSourceType = this.config.paramsSourceType || 'msg';
|
|
381
235
|
const paramsSource = this.config.paramsSource || 'parameters';
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
if (!
|
|
387
|
-
|
|
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);
|
|
388
246
|
}
|
|
247
|
+
return { query: finalQuery, params: this.currentParamsForErrorContext };
|
|
248
|
+
};
|
|
389
249
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
}
|
|
250
|
+
const executeUserQuery = async (connection, query, params) => {
|
|
251
|
+
// Appliquer le queryTimeout configuré
|
|
252
|
+
const configuredTimeout = parseInt(this.poolNode.config.queryTimeoutSeconds, 10);
|
|
253
|
+
if (configuredTimeout > 0) {
|
|
254
|
+
try { connection.queryTimeout = configuredTimeout; }
|
|
255
|
+
catch (e) { this.warn(`Could not set queryTimeout on connection: ${e.message}`); }
|
|
396
256
|
} else {
|
|
397
|
-
connection.queryTimeout = 0;
|
|
257
|
+
connection.queryTimeout = 0; // Infini ou défaut driver
|
|
398
258
|
}
|
|
399
259
|
|
|
400
260
|
this.status({ fill: "blue", shape: "dot", text: "executing..." });
|
|
@@ -407,90 +267,107 @@ module.exports = function (RED) {
|
|
|
407
267
|
}
|
|
408
268
|
};
|
|
409
269
|
|
|
410
|
-
let
|
|
411
|
-
let
|
|
270
|
+
let activeConnection = null; // Pour gérer la connexion active (pool ou fraîche)
|
|
271
|
+
let shouldProceedToTimedRetry = false;
|
|
272
|
+
let errorForTimedRetry = null;
|
|
412
273
|
|
|
413
|
-
try {
|
|
414
|
-
|
|
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()");
|
|
274
|
+
try { // Tentative Principale (avec connexion du pool)
|
|
275
|
+
const { query, params } = await getRenderedQueryAndParams();
|
|
419
276
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
}
|
|
277
|
+
this.status({ fill: "yellow", shape: "dot", text: "connecting (pool)..." });
|
|
278
|
+
activeConnection = await this.poolNode.connect();
|
|
279
|
+
await executeUserQuery(activeConnection, query, params);
|
|
280
|
+
|
|
281
|
+
if (activeConnection) { await activeConnection.close(); activeConnection = null; }
|
|
425
282
|
return done();
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
283
|
+
|
|
284
|
+
} catch (initialError) {
|
|
285
|
+
this.warn(`Initial attempt failed: ${initialError.message}`);
|
|
286
|
+
if (activeConnection) { // Si la connexion a été obtenue mais que executeUserQuery a échoué
|
|
287
|
+
const connStillGood = await testBasicConnectivity(activeConnection, this);
|
|
288
|
+
try { await activeConnection.close(); activeConnection = null; } catch(e){this.warn("Error closing pool conn after initial error: "+e.message);}
|
|
289
|
+
|
|
290
|
+
if (connStillGood) { // La connexion est bonne, l'erreur vient de la requête utilisateur
|
|
291
|
+
this.status({ fill: "red", shape: "ring", text: "SQL error" });
|
|
292
|
+
return done(this.enhanceError(initialError, this.currentQueryForErrorContext, this.currentParamsForErrorContext, "SQL Query Error"));
|
|
293
|
+
}
|
|
430
294
|
}
|
|
431
|
-
|
|
295
|
+
// Si on arrive ici, la connexion poolée a eu un problème (soit pour se connecter, soit SELECT 1 a échoué)
|
|
432
296
|
|
|
433
297
|
if (this.poolNode.config.retryFreshConnection) {
|
|
434
298
|
this.warn("Attempting retry with a fresh connection.");
|
|
435
299
|
this.status({ fill: "yellow", shape: "dot", text: "Retrying (fresh)..." });
|
|
436
|
-
let freshConnection;
|
|
437
300
|
try {
|
|
438
301
|
const freshConnectConfig = this.poolNode.getFreshConnectionConfig();
|
|
439
|
-
|
|
440
|
-
this.log("Fresh connection established
|
|
441
|
-
|
|
442
|
-
await
|
|
443
|
-
|
|
302
|
+
activeConnection = await odbcModule.connect(freshConnectConfig);
|
|
303
|
+
this.log("Fresh connection established.");
|
|
304
|
+
|
|
305
|
+
const freshConnGood = await testBasicConnectivity(activeConnection, this);
|
|
306
|
+
if (!freshConnGood) {
|
|
307
|
+
// Erreur de connectivité même sur une connexion fraîche
|
|
308
|
+
errorForTimedRetry = this.enhanceError(new Error("Basic connectivity (SELECT 1) failed on fresh connection."), this.currentQueryForErrorContext, this.currentParamsForErrorContext, "Fresh Connection Test Failed");
|
|
309
|
+
shouldProceedToTimedRetry = true;
|
|
310
|
+
throw errorForTimedRetry; // Va au catch externe de ce bloc try-fresh
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// La connexion fraîche est bonne, on retente la requête utilisateur originale
|
|
314
|
+
const { query, params } = await getRenderedQueryAndParams(); // Re-préparer au cas où
|
|
315
|
+
await executeUserQuery(activeConnection, query, params);
|
|
316
|
+
|
|
317
|
+
this.log("Query successful with fresh connection. Resetting pool.");
|
|
444
318
|
await this.poolNode.resetPool();
|
|
319
|
+
if (activeConnection) { await activeConnection.close(); activeConnection = null; }
|
|
320
|
+
return done(); // Succès !
|
|
321
|
+
|
|
322
|
+
} 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
|
+
if (activeConnection) { try { await activeConnection.close(); activeConnection = null; } catch(e){this.warn("Error closing fresh conn after error: "+e.message);} }
|
|
445
326
|
|
|
446
|
-
if (
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
errorAfterInitialAttempts = this.enhanceError(freshError, null, null, "Retry with fresh connection also failed");
|
|
453
|
-
} finally {
|
|
454
|
-
if (freshConnection) {
|
|
455
|
-
try { await freshConnection.close(); } catch(e) { this.warn("Error closing fresh connection in finally: " + e.message); }
|
|
327
|
+
if (shouldProceedToTimedRetry) { // Signifie que SELECT 1 sur la connexion fraîche a échoué
|
|
328
|
+
// errorForTimedRetry est déjà setté
|
|
329
|
+
} else {
|
|
330
|
+
// SELECT 1 sur connexion fraîche a réussi, mais la requête utilisateur a échoué. C'est une erreur SQL.
|
|
331
|
+
this.status({ fill: "red", shape: "ring", text: "SQL error (on retry)" });
|
|
332
|
+
return done(this.enhanceError(freshErrorOrConnectivityFail, this.currentQueryForErrorContext, this.currentParamsForErrorContext, "SQL Query Error (on fresh connection)"));
|
|
456
333
|
}
|
|
457
334
|
}
|
|
458
|
-
} else {
|
|
459
|
-
|
|
335
|
+
} else { // Pas de retryFreshConnection configuré, l'erreur initiale était donc un problème de connexion.
|
|
336
|
+
errorForTimedRetry = this.enhanceError(initialError, this.currentQueryForErrorContext, this.currentParamsForErrorContext, "Connection Error");
|
|
337
|
+
shouldProceedToTimedRetry = true;
|
|
460
338
|
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
if (
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Logique de Retry Temporisé
|
|
342
|
+
if (shouldProceedToTimedRetry && errorForTimedRetry) {
|
|
465
343
|
const retryDelaySeconds = parseInt(this.poolNode.config.retryDelay, 10);
|
|
466
344
|
if (retryDelaySeconds > 0) {
|
|
467
|
-
this.warn(`
|
|
345
|
+
this.warn(`Connection issue suspected. Scheduling retry in ${retryDelaySeconds} seconds. Error: ${errorForTimedRetry.message}`);
|
|
468
346
|
this.status({ fill: "red", shape: "ring", text: `Retry in ${retryDelaySeconds}s...` });
|
|
469
347
|
this.isAwaitingRetry = true;
|
|
470
348
|
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}.`);
|
|
349
|
+
this.isAwaitingRetry = false; this.retryTimer = null;
|
|
350
|
+
this.log(`Retry timer expired. Re-emitting message for node ${this.id || this.name}.`);
|
|
474
351
|
this.receive(msg);
|
|
475
352
|
}, retryDelaySeconds * 1000);
|
|
476
|
-
|
|
477
|
-
} else {
|
|
478
|
-
this.status({ fill: "red", shape: "ring", text: "
|
|
479
|
-
|
|
353
|
+
return done(); // Termine l'invocation actuelle du message
|
|
354
|
+
} else { // Pas de délai de retry, ou délai à 0
|
|
355
|
+
this.status({ fill: "red", shape: "ring", text: "Connection Error" });
|
|
356
|
+
return done(errorForTimedRetry);
|
|
480
357
|
}
|
|
358
|
+
} else if (errorForTimedRetry) { // Une erreur SQL a été identifiée et ne doit pas déclencher de retry de connexion
|
|
359
|
+
this.status({ fill: "red", shape: "ring", text: "Error (No Retry)" });
|
|
360
|
+
return done(errorForTimedRetry); // Devrait déjà avoir été fait
|
|
481
361
|
} else {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
if (done) return done();
|
|
362
|
+
// Normalement, on ne devrait pas arriver ici si done() a été appelé après un succès.
|
|
363
|
+
this.log("[ODBC Node] DEBUG: Reached end of on('input') without error or prior done(). Calling done().");
|
|
364
|
+
return done();
|
|
486
365
|
}
|
|
487
366
|
});
|
|
488
367
|
|
|
489
368
|
this.on("close", (done) => {
|
|
490
369
|
if (this.retryTimer) {
|
|
491
|
-
clearTimeout(this.retryTimer);
|
|
492
|
-
this.retryTimer = null;
|
|
493
|
-
this.isAwaitingRetry = false;
|
|
370
|
+
clearTimeout(this.retryTimer); this.retryTimer = null; this.isAwaitingRetry = false;
|
|
494
371
|
this.log("Cleared pending retry timer on node close/redeploy.");
|
|
495
372
|
}
|
|
496
373
|
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.0",
|
|
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",
|