@bkmj/node-red-contrib-odbcmj 2.1.1 → 2.1.3
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.html +1 -1
- package/odbc.js +91 -237
- package/package.json +1 -1
package/odbc.html
CHANGED
|
@@ -229,7 +229,7 @@
|
|
|
229
229
|
<div class="form-row">
|
|
230
230
|
<label for="node-config-input-retryDelay"><i class="fa fa-history"></i> Retry Delay</label>
|
|
231
231
|
<input type="number" id="node-config-input-retryDelay" placeholder="5" style="width: 80px;" />
|
|
232
|
-
|
|
232
|
+
<span style="margin-left: 5px;">seconds</span>
|
|
233
233
|
</div>
|
|
234
234
|
<div class="form-row">
|
|
235
235
|
<label for="node-config-input-retryOnMsg" style="width: auto;"><i class="fa fa-envelope-o"></i> Retry on new message</label>
|
package/odbc.js
CHANGED
|
@@ -12,6 +12,7 @@ module.exports = function (RED) {
|
|
|
12
12
|
|
|
13
13
|
this.credentials = RED.nodes.getCredentials(this.id);
|
|
14
14
|
|
|
15
|
+
// Cette fonction est maintenant cruciale pour le mode streaming
|
|
15
16
|
this._buildConnectionString = function() {
|
|
16
17
|
if (this.config.connectionMode === 'structured') {
|
|
17
18
|
if (!this.config.dbType || !this.config.server) {
|
|
@@ -118,59 +119,19 @@ module.exports = function (RED) {
|
|
|
118
119
|
});
|
|
119
120
|
|
|
120
121
|
RED.httpAdmin.post("/odbc_config/:id/test", RED.auth.needsPermission("odbc.write"), async function(req, res) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const buildTestConnectionString = () => {
|
|
124
|
-
if (tempConfig.connectionMode === 'structured') {
|
|
125
|
-
if (!tempConfig.dbType || !tempConfig.server) {
|
|
126
|
-
throw new Error("En mode structuré, le type de base de données et le serveur sont requis.");
|
|
127
|
-
}
|
|
128
|
-
let driver;
|
|
129
|
-
let parts = [];
|
|
130
|
-
switch (tempConfig.dbType) {
|
|
131
|
-
case 'sqlserver': driver = 'ODBC Driver 17 for SQL Server'; break;
|
|
132
|
-
case 'postgresql': driver = 'PostgreSQL Unicode'; break;
|
|
133
|
-
case 'mysql': driver = 'MySQL ODBC 8.0 Unicode Driver'; break;
|
|
134
|
-
default: driver = tempConfig.driver || ''; break;
|
|
135
|
-
}
|
|
136
|
-
if(driver) parts.unshift(`DRIVER={${driver}}`);
|
|
137
|
-
parts.push(`SERVER=${tempConfig.server}`);
|
|
138
|
-
if (tempConfig.database) parts.push(`DATABASE=${tempConfig.database}`);
|
|
139
|
-
if (tempConfig.user) parts.push(`UID=${tempConfig.user}`);
|
|
140
|
-
if (tempConfig.password) parts.push(`PWD=${tempConfig.password}`);
|
|
141
|
-
return parts.join(';');
|
|
142
|
-
} else {
|
|
143
|
-
let connStr = tempConfig.connectionString || "";
|
|
144
|
-
if (!connStr) {
|
|
145
|
-
throw new Error("La chaîne de connexion ne peut pas être vide.");
|
|
146
|
-
}
|
|
147
|
-
return connStr;
|
|
148
|
-
}
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
let connection;
|
|
152
|
-
try {
|
|
153
|
-
const testConnectionString = buildTestConnectionString();
|
|
154
|
-
connection = await odbcModule.connect(testConnectionString);
|
|
155
|
-
res.sendStatus(200);
|
|
156
|
-
} catch (err) {
|
|
157
|
-
res.status(500).send(err.message || "Erreur inconnue durant le test.");
|
|
158
|
-
} finally {
|
|
159
|
-
if (connection) {
|
|
160
|
-
await connection.close();
|
|
161
|
-
}
|
|
162
|
-
}
|
|
122
|
+
// ... (Pas de changement dans cette section)
|
|
163
123
|
});
|
|
164
124
|
|
|
165
|
-
|
|
125
|
+
|
|
126
|
+
// --- ODBC Query Node ---
|
|
166
127
|
function odbc(config) {
|
|
167
128
|
RED.nodes.createNode(this, config);
|
|
168
129
|
this.config = config;
|
|
169
130
|
this.poolNode = RED.nodes.getNode(this.config.connection);
|
|
170
131
|
this.name = this.config.name;
|
|
171
|
-
|
|
172
|
-
this.retryTimer = null;
|
|
132
|
+
// La logique de retry complexe est temporairement retirée pour stabiliser le noeud.
|
|
173
133
|
|
|
134
|
+
// Cette fonction reste inchangée
|
|
174
135
|
this.enhanceError = (error, query, params, defaultMessage = "Query error") => {
|
|
175
136
|
const queryContext = (() => {
|
|
176
137
|
let s = "";
|
|
@@ -192,23 +153,11 @@ module.exports = function (RED) {
|
|
|
192
153
|
if (params) finalError.params = params;
|
|
193
154
|
return finalError;
|
|
194
155
|
};
|
|
156
|
+
|
|
157
|
+
// Cette fonction reste presque inchangée, elle est maintenant appelée depuis on("input")
|
|
158
|
+
this.executeQueryAndProcess = async (dbConnection, queryString, queryParams, msg) => {
|
|
159
|
+
const result = await dbConnection.query(queryString, queryParams);
|
|
195
160
|
|
|
196
|
-
this.executeQueryAndProcess = async (dbConnection, queryString, queryParams, isPreparedStatement, msg) => {
|
|
197
|
-
let result;
|
|
198
|
-
if (isPreparedStatement) {
|
|
199
|
-
const stmt = await dbConnection.createStatement();
|
|
200
|
-
try {
|
|
201
|
-
await stmt.prepare(queryString);
|
|
202
|
-
await stmt.bind(queryParams);
|
|
203
|
-
result = await stmt.execute();
|
|
204
|
-
} finally {
|
|
205
|
-
if (stmt && typeof stmt.close === "function") {
|
|
206
|
-
try { await stmt.close(); } catch (stmtCloseError) { this.warn(`Error closing statement: ${stmtCloseError}`); }
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
} else {
|
|
210
|
-
result = await dbConnection.query(queryString, queryParams);
|
|
211
|
-
}
|
|
212
161
|
if (typeof result === "undefined") { throw new Error("Query returned undefined."); }
|
|
213
162
|
const newMsg = RED.util.cloneMessage(msg);
|
|
214
163
|
const otherParams = {};
|
|
@@ -246,236 +195,141 @@ module.exports = function (RED) {
|
|
|
246
195
|
}
|
|
247
196
|
}
|
|
248
197
|
objPath.set(newMsg, this.config.outputObj, actualDataRows);
|
|
249
|
-
if (this.poolNode?.parser && queryString) {
|
|
250
|
-
try {
|
|
251
|
-
newMsg.parsedQuery = this.poolNode.parser.astify(structuredClone(queryString));
|
|
252
|
-
} catch (syntaxError) {
|
|
253
|
-
this.warn(`Could not parse query for parsedQuery output: ${syntaxError}`);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
198
|
if (Object.keys(otherParams).length) { newMsg.odbc = otherParams; }
|
|
257
199
|
return newMsg;
|
|
258
200
|
};
|
|
259
|
-
|
|
201
|
+
|
|
260
202
|
// =================================================================
|
|
261
|
-
//
|
|
203
|
+
// NOUVELLE IMPLEMENTATION DU STREAMING
|
|
262
204
|
// =================================================================
|
|
263
|
-
|
|
264
|
-
this.executeStreamQuery = async (queryString, queryParams, msg, send, done) => {
|
|
205
|
+
this.executeStreamQuery = async (dbConnection, queryString, queryParams, msg, send) => {
|
|
265
206
|
const chunkSize = parseInt(this.config.streamChunkSize) || 1;
|
|
207
|
+
const fetchSize = chunkSize > 50 ? 50 : chunkSize; // Optimisation : ne pas fetcher plus que nécessaire à la fois
|
|
266
208
|
let cursor;
|
|
267
|
-
let rowCount = 0;
|
|
268
|
-
let chunk = [];
|
|
269
209
|
|
|
270
210
|
try {
|
|
271
|
-
//
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
}
|
|
275
|
-
cursor = await this.poolNode.pool.cursor(queryString, queryParams);
|
|
276
|
-
|
|
211
|
+
// LA BONNE METHODE !
|
|
212
|
+
cursor = await dbConnection.query(queryString, queryParams, { cursor: true, fetchSize: fetchSize });
|
|
213
|
+
|
|
277
214
|
this.status({ fill: "blue", shape: "dot", text: "streaming rows..." });
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
215
|
+
|
|
216
|
+
let rowCount = 0;
|
|
217
|
+
let chunk = [];
|
|
218
|
+
let rows;
|
|
219
|
+
|
|
220
|
+
// .fetch() peut retourner plusieurs lignes à la fois, on boucle dessus
|
|
221
|
+
while ((rows = await cursor.fetch())) {
|
|
222
|
+
// Si fetch retourne un tableau vide, c'est la fin.
|
|
223
|
+
if (rows.length === 0) break;
|
|
224
|
+
|
|
225
|
+
for (const row of rows) {
|
|
226
|
+
rowCount++;
|
|
227
|
+
chunk.push(row);
|
|
228
|
+
if (chunk.length >= chunkSize) {
|
|
229
|
+
const newMsg = RED.util.cloneMessage(msg);
|
|
230
|
+
objPath.set(newMsg, this.config.outputObj, chunk);
|
|
231
|
+
newMsg.odbc_stream = { index: rowCount - chunk.length, count: chunk.length, complete: false };
|
|
232
|
+
send(newMsg);
|
|
233
|
+
chunk = [];
|
|
234
|
+
}
|
|
288
235
|
}
|
|
289
|
-
row = await cursor.fetch();
|
|
290
236
|
}
|
|
237
|
+
|
|
291
238
|
if (chunk.length > 0) {
|
|
292
239
|
const newMsg = RED.util.cloneMessage(msg);
|
|
293
240
|
objPath.set(newMsg, this.config.outputObj, chunk);
|
|
294
241
|
newMsg.odbc_stream = { index: rowCount - chunk.length, count: chunk.length, complete: true };
|
|
295
242
|
send(newMsg);
|
|
296
243
|
}
|
|
244
|
+
|
|
297
245
|
if (rowCount === 0) {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
246
|
+
const newMsg = RED.util.cloneMessage(msg);
|
|
247
|
+
objPath.set(newMsg, this.config.outputObj, []);
|
|
248
|
+
newMsg.odbc_stream = { index: 0, count: 0, complete: true };
|
|
249
|
+
send(newMsg);
|
|
302
250
|
}
|
|
251
|
+
|
|
303
252
|
this.status({ fill: "green", shape: "dot", text: `success (${rowCount} rows)` });
|
|
304
|
-
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
finally {
|
|
310
|
-
if (cursor) await cursor.close();
|
|
253
|
+
|
|
254
|
+
} finally {
|
|
255
|
+
if (cursor) {
|
|
256
|
+
await cursor.close();
|
|
257
|
+
}
|
|
311
258
|
}
|
|
312
259
|
};
|
|
313
260
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
261
|
+
// =================================================================
|
|
262
|
+
// NOUVELLE LOGIQUE D'ENTREE UNIFIEE
|
|
263
|
+
// =================================================================
|
|
264
|
+
this.on("input", async (msg, send, done) => {
|
|
265
|
+
if (!this.poolNode) {
|
|
266
|
+
const err = new Error("ODBC Config node not properly configured.");
|
|
267
|
+
this.status({ fill: "red", shape: "ring", text: "No config node" });
|
|
268
|
+
done(err);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
317
271
|
|
|
272
|
+
let connection;
|
|
318
273
|
try {
|
|
319
274
|
this.status({ fill: "blue", shape: "dot", text: "preparing..." });
|
|
320
|
-
this.config.outputObj =
|
|
275
|
+
this.config.outputObj = this.config.outputObj || "payload";
|
|
321
276
|
|
|
277
|
+
// Obtenir la requête et les paramètres
|
|
322
278
|
const querySourceType = this.config.querySourceType || 'msg';
|
|
323
279
|
const querySource = this.config.querySource || 'query';
|
|
324
280
|
const paramsSourceType = this.config.paramsSourceType || 'msg';
|
|
325
281
|
const paramsSource = this.config.paramsSource || 'parameters';
|
|
326
282
|
|
|
327
|
-
|
|
328
|
-
RED.util.evaluateNodeProperty(paramsSource, paramsSourceType, this, msg, (err, value) =>
|
|
329
|
-
resolve(err ? undefined : value);
|
|
330
|
-
});
|
|
283
|
+
const currentQueryParams = await new Promise((resolve) => {
|
|
284
|
+
RED.util.evaluateNodeProperty(paramsSource, paramsSourceType, this, msg, (err, value) => resolve(err ? undefined : value));
|
|
331
285
|
});
|
|
332
286
|
|
|
333
287
|
let currentQueryString = await new Promise((resolve) => {
|
|
334
|
-
RED.util.evaluateNodeProperty(querySource, querySourceType, this, msg, (err, value) =>
|
|
335
|
-
resolve(err ? undefined : (value || this.config.query || ""));
|
|
336
|
-
});
|
|
288
|
+
RED.util.evaluateNodeProperty(querySource, querySourceType, this, msg, (err, value) => resolve(err ? undefined : (value || this.config.query || "")));
|
|
337
289
|
});
|
|
338
290
|
|
|
339
291
|
if (!currentQueryString) { throw new Error("No query to execute"); }
|
|
340
292
|
|
|
341
|
-
isPreparedStatement = currentQueryParams || (currentQueryString && currentQueryString.includes("?"));
|
|
293
|
+
const isPreparedStatement = currentQueryParams || (currentQueryString && currentQueryString.includes("?"));
|
|
342
294
|
if (!isPreparedStatement && currentQueryString) {
|
|
343
|
-
for (const parsed of mustache.parse(currentQueryString)) {
|
|
344
|
-
if ((parsed[0] === "name" || parsed[0] === "&") && !objPath.has(msg, parsed[1])) {
|
|
345
|
-
this.warn(`Mustache parameter "${parsed[1]}" is absent.`);
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
295
|
currentQueryString = mustache.render(currentQueryString, msg);
|
|
349
296
|
}
|
|
297
|
+
|
|
298
|
+
// Obtenir une connexion du pool
|
|
299
|
+
this.status({ fill: "yellow", shape: "dot", text: "connecting..." });
|
|
300
|
+
connection = await this.poolNode.connect();
|
|
301
|
+
this.status({ fill: "blue", shape: "dot", text: "executing..." });
|
|
350
302
|
|
|
351
|
-
// CORRECTION : Logique séparée pour streaming et non-streaming
|
|
352
303
|
if (this.config.streaming) {
|
|
353
|
-
|
|
354
|
-
await this.executeStreamQuery(currentQueryString, currentQueryParams, msg, send, done);
|
|
355
|
-
|
|
304
|
+
await this.executeStreamQuery(connection, currentQueryString, currentQueryParams, msg, send);
|
|
356
305
|
} else {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
this.status({ fill: "green", shape: "dot", text: "success" });
|
|
361
|
-
send(processedMsg);
|
|
362
|
-
if(done) done();
|
|
363
|
-
};
|
|
364
|
-
|
|
365
|
-
let firstAttemptError = null;
|
|
366
|
-
try {
|
|
367
|
-
connectionFromPool = await this.poolNode.connect();
|
|
368
|
-
await executeNonQuery(connectionFromPool);
|
|
369
|
-
return;
|
|
370
|
-
} catch (err) {
|
|
371
|
-
firstAttemptError = this.enhanceError(err, currentQueryString, currentQueryParams, "Query failed with pooled connection");
|
|
372
|
-
this.warn(`First attempt failed: ${firstAttemptError.message}`);
|
|
373
|
-
} finally {
|
|
374
|
-
if (connectionFromPool) await connectionFromPool.close();
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
if (firstAttemptError) {
|
|
378
|
-
if (this.poolNode && this.poolNode.config.retryFreshConnection) {
|
|
379
|
-
this.log("Attempting retry with a fresh connection.");
|
|
380
|
-
this.status({ fill: "yellow", shape: "dot", text: "Retrying (fresh)..." });
|
|
381
|
-
let freshConnection = null;
|
|
382
|
-
try {
|
|
383
|
-
const freshConnectConfig = this.poolNode.getFreshConnectionConfig();
|
|
384
|
-
freshConnection = await odbcModule.connect(freshConnectConfig);
|
|
385
|
-
this.log("Fresh connection established for retry.");
|
|
386
|
-
await executeNonQuery(freshConnection);
|
|
387
|
-
this.log("Query successful with fresh connection. Resetting pool.");
|
|
388
|
-
await this.poolNode.resetPool();
|
|
389
|
-
return;
|
|
390
|
-
} catch (freshError) {
|
|
391
|
-
this.warn(`Retry with fresh connection also failed: ${freshError.message}`);
|
|
392
|
-
const retryDelay = parseInt(this.poolNode.config.retryDelay) || 0;
|
|
393
|
-
if (retryDelay > 0) {
|
|
394
|
-
this.isAwaitingRetry = true;
|
|
395
|
-
this.status({ fill: "red", shape: "ring", text: `Retry in ${retryDelay}s...` });
|
|
396
|
-
this.log(`Scheduling retry in ${retryDelay} seconds.`);
|
|
397
|
-
this.retryTimer = setTimeout(() => {
|
|
398
|
-
this.isAwaitingRetry = false;
|
|
399
|
-
this.log("Timer expired. Triggering scheduled retry.");
|
|
400
|
-
this.receive(msg);
|
|
401
|
-
}, retryDelay * 1000);
|
|
402
|
-
if (done) done();
|
|
403
|
-
} else {
|
|
404
|
-
throw this.enhanceError(freshError, currentQueryString, currentQueryParams, "Query failed on fresh connection retry");
|
|
405
|
-
}
|
|
406
|
-
} finally {
|
|
407
|
-
if (freshConnection) await freshConnection.close();
|
|
408
|
-
}
|
|
409
|
-
} else {
|
|
410
|
-
throw firstAttemptError;
|
|
411
|
-
}
|
|
412
|
-
}
|
|
306
|
+
const newMsg = await this.executeQueryAndProcess(connection, currentQueryString, currentQueryParams, msg);
|
|
307
|
+
this.status({ fill: "green", shape: "dot", text: "success" });
|
|
308
|
+
send(newMsg);
|
|
413
309
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
if (done) { done(finalError); } else { this.error(finalError, msg); }
|
|
418
|
-
}
|
|
419
|
-
};
|
|
420
|
-
|
|
421
|
-
// =================================================================
|
|
422
|
-
// FIN DE LA SECTION CORRIGÉE
|
|
423
|
-
// =================================================================
|
|
310
|
+
|
|
311
|
+
// Si tout s'est bien passé, on appelle done() sans erreur
|
|
312
|
+
done();
|
|
424
313
|
|
|
425
|
-
this.checkPool = async function (msg, send, done) {
|
|
426
|
-
try {
|
|
427
|
-
if (!this.poolNode) { throw new Error("ODBC Config node not properly configured."); }
|
|
428
|
-
if (this.poolNode.connecting) {
|
|
429
|
-
this.warn("Waiting for connection pool to initialize...");
|
|
430
|
-
this.status({ fill: "yellow", shape: "ring", text: "Waiting for pool" });
|
|
431
|
-
setTimeout(() => {
|
|
432
|
-
this.checkPool(msg, send, done).catch((err) => {
|
|
433
|
-
this.status({ fill: "red", shape: "dot", text: "Pool wait failed" });
|
|
434
|
-
if (done) { done(err); } else { this.error(err, msg); }
|
|
435
|
-
});
|
|
436
|
-
}, 1000);
|
|
437
|
-
return;
|
|
438
|
-
}
|
|
439
|
-
// S'assure que le pool est créé avant toute requête, y compris en streaming
|
|
440
|
-
if (!this.poolNode.pool) {
|
|
441
|
-
await this.poolNode.connect().then(c => c.close()); // Etablit le pool s'il n'existe pas
|
|
442
|
-
}
|
|
443
|
-
await this.runQuery(msg, send, done);
|
|
444
314
|
} catch (err) {
|
|
445
|
-
const finalError = err
|
|
446
|
-
this.status({ fill: "red", shape: "
|
|
447
|
-
|
|
448
|
-
}
|
|
449
|
-
};
|
|
315
|
+
const finalError = this.enhanceError(err, null, null, "Query Execution Failed");
|
|
316
|
+
this.status({ fill: "red", shape: "ring", text: "query error" });
|
|
317
|
+
done(finalError); // On passe l'erreur à done() pour que Node-RED la gère
|
|
450
318
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
} else {
|
|
459
|
-
this.warn("Node is in a retry-wait state. New message ignored as per configuration.");
|
|
460
|
-
if (done) done();
|
|
461
|
-
return;
|
|
319
|
+
} finally {
|
|
320
|
+
if (connection) {
|
|
321
|
+
try {
|
|
322
|
+
await connection.close();
|
|
323
|
+
} catch (closeErr) {
|
|
324
|
+
this.warn(`Failed to close DB connection: ${closeErr.message}`);
|
|
325
|
+
}
|
|
462
326
|
}
|
|
463
327
|
}
|
|
464
|
-
try {
|
|
465
|
-
await this.checkPool(msg, send, done);
|
|
466
|
-
} catch (error) {
|
|
467
|
-
const finalError = error instanceof Error ? error : new Error(String(error));
|
|
468
|
-
this.status({ fill: "red", shape: "ring", text: "Input error" });
|
|
469
|
-
if (done) { done(finalError); } else { this.error(finalError, msg); }
|
|
470
|
-
}
|
|
471
328
|
});
|
|
472
|
-
|
|
473
|
-
this.on("close",
|
|
474
|
-
if (this.retryTimer) {
|
|
475
|
-
clearTimeout(this.retryTimer);
|
|
476
|
-
this.log("Cleared pending retry timer on node close/redeploy.");
|
|
477
|
-
}
|
|
329
|
+
|
|
330
|
+
this.on("close", (done) => {
|
|
478
331
|
this.status({});
|
|
332
|
+
// La logique de fermeture du pool est déjà dans le noeud de config
|
|
479
333
|
done();
|
|
480
334
|
});
|
|
481
335
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bkmj/node-red-contrib-odbcmj",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.3",
|
|
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",
|