@bkmj/node-red-contrib-odbcmj 2.3.0 → 2.3.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 +140 -53
- package/package.json +1 -1
package/odbc.js
CHANGED
|
@@ -12,10 +12,11 @@ module.exports = function (RED) {
|
|
|
12
12
|
|
|
13
13
|
this.credentials = RED.nodes.getCredentials(this.id);
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
this.config.queryTimeoutSeconds = parseInt(config.queryTimeoutSeconds, 10);
|
|
16
|
+
if (isNaN(this.config.queryTimeoutSeconds) || this.config.queryTimeoutSeconds < 0) {
|
|
17
|
+
this.config.queryTimeoutSeconds = 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
19
20
|
this.closeOperationTimeout = 10000; // 10 secondes
|
|
20
21
|
|
|
21
22
|
this._buildConnectionString = function() {
|
|
@@ -36,8 +37,6 @@ module.exports = function (RED) {
|
|
|
36
37
|
if (this.config.database) parts.push(`DATABASE=${this.config.database}`);
|
|
37
38
|
if (this.config.user) parts.push(`UID=${this.config.user}`);
|
|
38
39
|
if (this.credentials && this.credentials.password) parts.push(`PWD=${this.credentials.password}`);
|
|
39
|
-
// NOUVEAU: Potentiellement ajouter des options de timeout ici si le driver les supporte dans la CS
|
|
40
|
-
// Exemple (non standard, dépend du driver): if (this.config.loginTimeout > 0) parts.push(`LoginTimeout=${this.config.loginTimeout}`);
|
|
41
40
|
return parts.join(';');
|
|
42
41
|
} else {
|
|
43
42
|
let connStr = this.config.connectionString || "";
|
|
@@ -53,19 +52,18 @@ module.exports = function (RED) {
|
|
|
53
52
|
const finalConnectionString = this._buildConnectionString();
|
|
54
53
|
if (!finalConnectionString) throw new Error("La chaîne de connexion est vide.");
|
|
55
54
|
|
|
56
|
-
const poolParams = {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
55
|
+
const poolParams = {
|
|
56
|
+
connectionString: finalConnectionString,
|
|
57
|
+
initialSize: parseInt(this.config.initialSize, 10) || undefined,
|
|
58
|
+
incrementSize: parseInt(this.config.incrementSize, 10) || undefined,
|
|
59
|
+
maxSize: parseInt(this.config.maxSize, 10) || undefined,
|
|
60
|
+
shrink: typeof this.config.shrink === 'boolean' ? this.config.shrink : true,
|
|
61
|
+
connectionTimeout: (parseInt(this.config.connectionTimeout, 10) * 1000) || undefined,
|
|
62
|
+
loginTimeout: parseInt(this.config.loginTimeout, 10) || undefined
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
Object.keys(poolParams).forEach(key => poolParams[key] === undefined && delete poolParams[key]);
|
|
63
66
|
|
|
64
|
-
// NOUVEAU: Debug des paramètres du pool
|
|
65
|
-
// this.log(`Initializing pool with params: ${JSON.stringify(poolParams)}`);
|
|
66
|
-
|
|
67
|
-
// Potentiel point de blocage si odbcModule.pool() ne gère pas bien les erreurs de driver/connexion
|
|
68
|
-
// Il n'y a pas de timeout direct pour odbcModule.pool() lui-même
|
|
69
67
|
this.pool = await odbcModule.pool(poolParams);
|
|
70
68
|
this.connecting = false;
|
|
71
69
|
this.status({ fill: "green", shape: "dot", text: "Pool ready" });
|
|
@@ -78,8 +76,6 @@ module.exports = function (RED) {
|
|
|
78
76
|
}
|
|
79
77
|
}
|
|
80
78
|
try {
|
|
81
|
-
// odbc.pool.connect() peut aussi théoriquement bloquer, mais devrait utiliser
|
|
82
|
-
// les timeouts des connexions individuelles ou le connectionTimeout du pool (pour l'attente d'une connexion dispo)
|
|
83
79
|
return await this.pool.connect();
|
|
84
80
|
} catch (poolConnectError) {
|
|
85
81
|
this.error(`Error connecting to pool: ${poolConnectError.message}`, poolConnectError);
|
|
@@ -89,16 +85,13 @@ module.exports = function (RED) {
|
|
|
89
85
|
};
|
|
90
86
|
|
|
91
87
|
this.getFreshConnectionConfig = function() {
|
|
92
|
-
// Ces timeouts sont pour odbcModule.connect (connexion unique)
|
|
93
88
|
return {
|
|
94
89
|
connectionString: this._buildConnectionString(),
|
|
95
|
-
connectionTimeout: 0,
|
|
96
|
-
|
|
97
|
-
loginTimeout: parseInt(this.config.loginTimeout, 10) || 5, // Timeout pour l'établissement de la connexion. 5s par défaut.
|
|
90
|
+
connectionTimeout: 0,
|
|
91
|
+
loginTimeout: parseInt(this.config.loginTimeout, 10) || 5,
|
|
98
92
|
};
|
|
99
93
|
};
|
|
100
94
|
|
|
101
|
-
// MODIFIÉ: Ajout de timeout pour pool.close()
|
|
102
95
|
this.resetPool = async () => {
|
|
103
96
|
if (this.pool) {
|
|
104
97
|
this.log("Resetting connection pool.");
|
|
@@ -129,7 +122,6 @@ module.exports = function (RED) {
|
|
|
129
122
|
}
|
|
130
123
|
};
|
|
131
124
|
|
|
132
|
-
// MODIFIÉ: Ajout de timeout pour pool.close()
|
|
133
125
|
this.on("close", async (removed, done) => {
|
|
134
126
|
this.log("Closing ODBC config node. Attempting to close pool.");
|
|
135
127
|
if (this.pool) {
|
|
@@ -144,7 +136,7 @@ module.exports = function (RED) {
|
|
|
144
136
|
} catch (error) {
|
|
145
137
|
this.error(`Error or timeout closing connection pool on node close: ${error.message}`, error);
|
|
146
138
|
} finally {
|
|
147
|
-
this.pool = null;
|
|
139
|
+
this.pool = null;
|
|
148
140
|
}
|
|
149
141
|
}
|
|
150
142
|
done();
|
|
@@ -189,20 +181,20 @@ module.exports = function (RED) {
|
|
|
189
181
|
let connection;
|
|
190
182
|
try {
|
|
191
183
|
const testConnectionString = buildTestConnectionString();
|
|
192
|
-
console.log("[ODBC Test] Attempting to connect with string:", testConnectionString);
|
|
184
|
+
// console.log("[ODBC Test] Attempting to connect with string:", testConnectionString); // Conservé pour debug si besoin
|
|
193
185
|
|
|
194
186
|
const connectionOptions = {
|
|
195
187
|
connectionString: testConnectionString,
|
|
196
|
-
loginTimeout: 10
|
|
188
|
+
loginTimeout: 10
|
|
197
189
|
};
|
|
198
190
|
connection = await odbcModule.connect(connectionOptions);
|
|
199
191
|
res.sendStatus(200);
|
|
200
192
|
} catch (err) {
|
|
201
|
-
console.error("[ODBC Test] Connection failed:", err);
|
|
193
|
+
// console.error("[ODBC Test] Connection failed:", err); // Conservé pour debug si besoin
|
|
202
194
|
res.status(500).send(err.message || "Erreur inconnue durant le test.");
|
|
203
195
|
} finally {
|
|
204
196
|
if (connection) {
|
|
205
|
-
await connection.close();
|
|
197
|
+
await connection.close();
|
|
206
198
|
}
|
|
207
199
|
}
|
|
208
200
|
});
|
|
@@ -215,21 +207,95 @@ module.exports = function (RED) {
|
|
|
215
207
|
this.name = this.config.name;
|
|
216
208
|
this.isAwaitingRetry = false;
|
|
217
209
|
this.retryTimer = null;
|
|
210
|
+
this.cursorCloseOperationTimeout = 5000;
|
|
218
211
|
|
|
219
|
-
|
|
220
|
-
|
|
212
|
+
this.enhanceError = (error, query, params, defaultMessage = "Query error") => {
|
|
213
|
+
const queryContext = (() => {
|
|
214
|
+
let s = "";
|
|
215
|
+
if (query || params) {
|
|
216
|
+
s += " {";
|
|
217
|
+
if (query) s += `"query": '${query.substring(0, 100)}${query.length > 100 ? "..." : ""}'`;
|
|
218
|
+
if (params) s += `, "params": '${JSON.stringify(params)}'`;
|
|
219
|
+
s += "}";
|
|
220
|
+
return s;
|
|
221
|
+
}
|
|
222
|
+
return "";
|
|
223
|
+
})();
|
|
224
|
+
let finalError;
|
|
225
|
+
if (typeof error === "object" && error !== null && error.message) { finalError = error; }
|
|
226
|
+
else if (typeof error === "string") { finalError = new Error(error); }
|
|
227
|
+
else { finalError = new Error(defaultMessage); }
|
|
228
|
+
finalError.message = `${finalError.message}${queryContext}`;
|
|
229
|
+
if (query) finalError.query = query;
|
|
230
|
+
if (params) finalError.params = params;
|
|
231
|
+
return finalError;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
// MODIFIÉ : Nettoyage des objets de résultat
|
|
235
|
+
this.executeQueryAndProcess = async (dbConnection, queryString, queryParams, msg) => {
|
|
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 = [];
|
|
221
243
|
|
|
222
|
-
|
|
223
|
-
|
|
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
|
+
});
|
|
252
|
+
|
|
253
|
+
for (const [key, value] of Object.entries(result)) {
|
|
254
|
+
if (isNaN(parseInt(key))) {
|
|
255
|
+
otherParams[key] = value;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
for (const [key, value] of Object.entries(result)) {
|
|
260
|
+
otherParams[key] = value;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
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
|
+
};
|
|
224
292
|
|
|
225
|
-
// MODIFIÉ: Ajout de timeout pour cursor.close()
|
|
226
293
|
this.executeStreamQuery = async (dbConnection, queryString, queryParams, msg, send) => {
|
|
227
294
|
const chunkSize = parseInt(this.config.streamChunkSize) || 1;
|
|
228
295
|
const fetchSize = chunkSize > 100 ? 100 : chunkSize;
|
|
229
296
|
let cursor;
|
|
230
297
|
|
|
231
298
|
try {
|
|
232
|
-
// dbConnection.query() utilisera le dbConnection.queryTimeout défini plus bas
|
|
233
299
|
cursor = await dbConnection.query(queryString, queryParams, { cursor: true, fetchSize: fetchSize });
|
|
234
300
|
this.status({ fill: "blue", shape: "dot", text: "streaming rows..." });
|
|
235
301
|
|
|
@@ -237,16 +303,17 @@ module.exports = function (RED) {
|
|
|
237
303
|
let chunk = [];
|
|
238
304
|
|
|
239
305
|
while (true) {
|
|
240
|
-
// cursor.fetch() pourrait aussi théoriquement bloquer, mais c'est plus rare si la requête initiale a fonctionné
|
|
241
306
|
const rows = await cursor.fetch();
|
|
242
307
|
if (!rows || rows.length === 0) { break; }
|
|
243
308
|
|
|
244
309
|
for (const row of rows) {
|
|
245
310
|
rowCount++;
|
|
246
|
-
|
|
311
|
+
// Nettoyer chaque ligne aussi pour le streaming
|
|
312
|
+
const cleanRow = (typeof row === 'object' && row !== null) ? { ...row } : row;
|
|
313
|
+
chunk.push(cleanRow);
|
|
247
314
|
if (chunk.length >= chunkSize) {
|
|
248
315
|
const newMsg = RED.util.cloneMessage(msg);
|
|
249
|
-
objPath.set(newMsg, this.config.outputObj, chunk);
|
|
316
|
+
objPath.set(newMsg, this.config.outputObj || "payload", chunk);
|
|
250
317
|
newMsg.odbc_stream = { index: rowCount - chunk.length, count: chunk.length, complete: false };
|
|
251
318
|
send(newMsg);
|
|
252
319
|
chunk = [];
|
|
@@ -256,13 +323,13 @@ module.exports = function (RED) {
|
|
|
256
323
|
|
|
257
324
|
if (chunk.length > 0) {
|
|
258
325
|
const newMsg = RED.util.cloneMessage(msg);
|
|
259
|
-
objPath.set(newMsg, this.config.outputObj, chunk);
|
|
326
|
+
objPath.set(newMsg, this.config.outputObj || "payload", chunk);
|
|
260
327
|
newMsg.odbc_stream = { index: rowCount - chunk.length, count: chunk.length, complete: false };
|
|
261
328
|
send(newMsg);
|
|
262
329
|
}
|
|
263
330
|
|
|
264
331
|
const finalMsg = RED.util.cloneMessage(msg);
|
|
265
|
-
objPath.set(finalMsg, this.config.outputObj, []);
|
|
332
|
+
objPath.set(finalMsg, this.config.outputObj || "payload", []);
|
|
266
333
|
finalMsg.odbc_stream = { index: rowCount, count: 0, complete: true };
|
|
267
334
|
send(finalMsg);
|
|
268
335
|
|
|
@@ -271,7 +338,6 @@ module.exports = function (RED) {
|
|
|
271
338
|
} finally {
|
|
272
339
|
if (cursor) {
|
|
273
340
|
try {
|
|
274
|
-
// NOUVEAU: Timeout pour la fermeture du curseur
|
|
275
341
|
await Promise.race([
|
|
276
342
|
cursor.close(),
|
|
277
343
|
new Promise((_, reject) =>
|
|
@@ -285,7 +351,6 @@ module.exports = function (RED) {
|
|
|
285
351
|
}
|
|
286
352
|
};
|
|
287
353
|
|
|
288
|
-
// MODIFIÉ: Ajout de la définition du queryTimeout sur la connexion
|
|
289
354
|
this.on("input", async (msg, send, done) => {
|
|
290
355
|
if (this.isAwaitingRetry) {
|
|
291
356
|
if (this.poolNode && this.poolNode.config.retryOnMsg === true) {
|
|
@@ -308,7 +373,8 @@ module.exports = function (RED) {
|
|
|
308
373
|
}
|
|
309
374
|
|
|
310
375
|
const executeWithConnection = async (connection) => {
|
|
311
|
-
|
|
376
|
+
const outputProperty = this.config.outputObj || "payload"; // S'assurer que outputProperty est défini
|
|
377
|
+
|
|
312
378
|
const querySourceType = this.config.querySourceType || 'msg';
|
|
313
379
|
const querySource = this.config.querySource || 'query';
|
|
314
380
|
const paramsSourceType = this.config.paramsSourceType || 'msg';
|
|
@@ -321,16 +387,14 @@ module.exports = function (RED) {
|
|
|
321
387
|
query = mustache.render(query, msg);
|
|
322
388
|
}
|
|
323
389
|
|
|
324
|
-
// NOUVEAU: Appliquer le queryTimeout à la connexion avant exécution
|
|
325
390
|
if (this.poolNode.config.queryTimeoutSeconds > 0) {
|
|
326
391
|
try {
|
|
327
392
|
connection.queryTimeout = parseInt(this.poolNode.config.queryTimeoutSeconds, 10);
|
|
328
|
-
// this.log(`Query timeout set to ${connection.queryTimeout}s for this execution.`);
|
|
329
393
|
} catch (e) {
|
|
330
394
|
this.warn(`Could not set queryTimeout on connection: ${e.message}`);
|
|
331
395
|
}
|
|
332
396
|
} else {
|
|
333
|
-
connection.queryTimeout = 0;
|
|
397
|
+
connection.queryTimeout = 0;
|
|
334
398
|
}
|
|
335
399
|
|
|
336
400
|
this.status({ fill: "blue", shape: "dot", text: "executing..." });
|
|
@@ -349,10 +413,23 @@ module.exports = function (RED) {
|
|
|
349
413
|
try {
|
|
350
414
|
this.status({ fill: "yellow", shape: "dot", text: "connecting..." });
|
|
351
415
|
connectionFromPool = await this.poolNode.connect();
|
|
416
|
+
//this.log("[ODBC Node] DEBUG: Avant executeWithConnection (pooled)"); // Logs de debug
|
|
352
417
|
await executeWithConnection(connectionFromPool);
|
|
418
|
+
//this.log("[ODBC Node] DEBUG: Après executeWithConnection (pooled), avant fermeture et done()");
|
|
419
|
+
|
|
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
|
+
}
|
|
353
425
|
return done();
|
|
354
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
|
+
}
|
|
355
431
|
this.warn(`First attempt with pooled connection failed: ${poolError.message}`);
|
|
432
|
+
|
|
356
433
|
if (this.poolNode.config.retryFreshConnection) {
|
|
357
434
|
this.warn("Attempting retry with a fresh connection.");
|
|
358
435
|
this.status({ fill: "yellow", shape: "dot", text: "Retrying (fresh)..." });
|
|
@@ -361,21 +438,28 @@ module.exports = function (RED) {
|
|
|
361
438
|
const freshConnectConfig = this.poolNode.getFreshConnectionConfig();
|
|
362
439
|
freshConnection = await odbcModule.connect(freshConnectConfig);
|
|
363
440
|
this.log("Fresh connection established for retry.");
|
|
441
|
+
//this.log("[ODBC Node] DEBUG: Avant executeWithConnection (fresh)");
|
|
364
442
|
await executeWithConnection(freshConnection);
|
|
365
|
-
this.log("
|
|
443
|
+
//this.log("[ODBC Node] DEBUG: Après executeWithConnection (fresh), avant resetPool et done()");
|
|
366
444
|
await this.poolNode.resetPool();
|
|
445
|
+
|
|
446
|
+
if (freshConnection) {
|
|
447
|
+
await freshConnection.close();
|
|
448
|
+
freshConnection = null;
|
|
449
|
+
}
|
|
367
450
|
return done();
|
|
368
451
|
} catch (freshError) {
|
|
369
452
|
errorAfterInitialAttempts = this.enhanceError(freshError, null, null, "Retry with fresh connection also failed");
|
|
370
453
|
} finally {
|
|
371
|
-
if (freshConnection)
|
|
454
|
+
if (freshConnection) {
|
|
455
|
+
try { await freshConnection.close(); } catch(e) { this.warn("Error closing fresh connection in finally: " + e.message); }
|
|
456
|
+
}
|
|
372
457
|
}
|
|
373
458
|
} else {
|
|
374
459
|
errorAfterInitialAttempts = this.enhanceError(poolError);
|
|
375
460
|
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
}
|
|
461
|
+
}
|
|
462
|
+
// Le 'finally' qui fermait connectionFromPool est retiré ici car géré dans chaque chemin
|
|
379
463
|
|
|
380
464
|
if (errorAfterInitialAttempts) {
|
|
381
465
|
const retryDelaySeconds = parseInt(this.poolNode.config.retryDelay, 10);
|
|
@@ -395,6 +479,9 @@ module.exports = function (RED) {
|
|
|
395
479
|
if (done) return done(errorAfterInitialAttempts);
|
|
396
480
|
}
|
|
397
481
|
} else {
|
|
482
|
+
// Si on arrive ici SANS errorAfterInitialAttempts, c'est que done() aurait dû être appelé.
|
|
483
|
+
// C'est une situation anormale, mais assurons-nous que done() soit appelé.
|
|
484
|
+
// this.log("[ODBC Node] DEBUG: Atteint la fin de on('input') sans erreur signalée et done() non appelé plus tôt. Appel de done().");
|
|
398
485
|
if (done) return done();
|
|
399
486
|
}
|
|
400
487
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bkmj/node-red-contrib-odbcmj",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.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",
|