@bkmj/node-red-contrib-odbcmj 2.1.1 → 2.1.2
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/README.md +4 -0
- package/odbc.html +5 -1
- package/odbc.js +32 -160
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -49,6 +49,8 @@ A **Test Connection** button in the configuration panel allows you to instantly
|
|
|
49
49
|
|
|
50
50
|
#### Pool Options
|
|
51
51
|
|
|
52
|
+
> **Note Importante :** Ces options de pool s'appliquent uniquement aux requêtes standards (**non-streamées**). Les requêtes en mode streaming gèrent leur propre connexion temporaire pour chaque exécution et n'utilisent **pas** le pool.
|
|
53
|
+
|
|
52
54
|
- **`initialSize`** `<number>` (optional): The number of connections to create when the pool is initialized. Default: 5.
|
|
53
55
|
- **`incrementSize`** `<number>` (optional): The number of connections to create when the pool is exhausted. Default: 5.
|
|
54
56
|
- **`maxSize`** `<number>` (optional): The maximum number of connections allowed in the pool. Default: 15.
|
|
@@ -58,6 +60,8 @@ A **Test Connection** button in the configuration panel allows you to instantly
|
|
|
58
60
|
|
|
59
61
|
#### Error Handling & Retry
|
|
60
62
|
|
|
63
|
+
> **Note Importante :** Cette logique de nouvelle tentative et de réinitialisation du pool s'applique aux requêtes standards (**non-streamées**). Une requête en mode streaming qui échoue remontera une erreur directement, sans déclencher ce mécanisme spécifique de nouvelle tentative.
|
|
64
|
+
|
|
61
65
|
- **`retryFreshConnection`** `<boolean>` (optional): If a query fails, the node will retry once with a brand new connection. If this succeeds, the entire connection pool is reset to clear any stale connections. Default: false.
|
|
62
66
|
- **`retryDelay`** `<number>` (optional): If both the pooled and the fresh connection attempts fail, this sets a delay in seconds before another retry is attempted. A value of **0** disables further automatic retries. Default: 5.
|
|
63
67
|
- **`retryOnMsg`** `<boolean>` (optional): If the node is waiting for a timed retry, a new incoming message can override the timer and trigger an immediate retry. Default: true.
|
package/odbc.html
CHANGED
|
@@ -387,13 +387,17 @@ This mode gives you full control for complex or non-standard connection strings.
|
|
|
387
387
|
A **Test Connection** button in the configuration panel allows you to instantly verify your settings without deploying the flow.
|
|
388
388
|
|
|
389
389
|
### Pool Options
|
|
390
|
+
> **Important Note:** These pool options only apply to standard (non-streaming) queries. Streaming queries manage their own temporary connection for each execution and do **not** use the connection pool.
|
|
391
|
+
|
|
390
392
|
- **`initialSize`**: The number of connections to create when the pool is initialized. Default: 5.
|
|
391
393
|
- **`maxSize`**: The maximum number of connections allowed in the pool. Default: 15.
|
|
392
394
|
- (See `odbc` package documentation for more details on pool options).
|
|
393
395
|
|
|
394
396
|
### Error Handling & Retry
|
|
397
|
+
> **Important Note:** This retry logic applies to standard (non-streaming) queries. A streaming query that fails will report an error directly, without triggering this specific retry mechanism.
|
|
398
|
+
|
|
395
399
|
- **`retryFreshConnection`**: If a query fails, the node will retry once with a brand new connection. If this succeeds, the entire connection pool is reset to clear any stale connections.
|
|
396
|
-
- **`retryDelay
|
|
400
|
+
- **`retryDelay`**`: If both attempts fail, this sets a delay in seconds before another retry is attempted. A value of **0** disables further automatic retries.
|
|
397
401
|
- **`retryOnMsg`**: If the node is waiting for a timed retry, a new incoming message can override the timer and trigger an immediate retry.
|
|
398
402
|
|
|
399
403
|
### Advanced
|
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,48 +119,7 @@ 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
|
// --- ODBC Query Node ---
|
|
@@ -172,89 +132,11 @@ module.exports = function (RED) {
|
|
|
172
132
|
this.retryTimer = null;
|
|
173
133
|
|
|
174
134
|
this.enhanceError = (error, query, params, defaultMessage = "Query error") => {
|
|
175
|
-
|
|
176
|
-
let s = "";
|
|
177
|
-
if (query || params) {
|
|
178
|
-
s += " {";
|
|
179
|
-
if (query) s += `"query": '${query.substring(0, 100)}${query.length > 100 ? "..." : ""}'`;
|
|
180
|
-
if (params) s += `, "params": '${JSON.stringify(params)}'`;
|
|
181
|
-
s += "}";
|
|
182
|
-
return s;
|
|
183
|
-
}
|
|
184
|
-
return "";
|
|
185
|
-
})();
|
|
186
|
-
let finalError;
|
|
187
|
-
if (typeof error === "object" && error !== null && error.message) { finalError = error; }
|
|
188
|
-
else if (typeof error === "string") { finalError = new Error(error); }
|
|
189
|
-
else { finalError = new Error(defaultMessage); }
|
|
190
|
-
finalError.message = `${finalError.message}${queryContext}`;
|
|
191
|
-
if (query) finalError.query = query;
|
|
192
|
-
if (params) finalError.params = params;
|
|
193
|
-
return finalError;
|
|
135
|
+
// ... (Pas de changement dans cette section)
|
|
194
136
|
};
|
|
195
137
|
|
|
196
138
|
this.executeQueryAndProcess = async (dbConnection, queryString, queryParams, isPreparedStatement, msg) => {
|
|
197
|
-
|
|
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
|
-
if (typeof result === "undefined") { throw new Error("Query returned undefined."); }
|
|
213
|
-
const newMsg = RED.util.cloneMessage(msg);
|
|
214
|
-
const otherParams = {};
|
|
215
|
-
let actualDataRows = [];
|
|
216
|
-
if (result !== null && typeof result === "object") {
|
|
217
|
-
if (Array.isArray(result)) {
|
|
218
|
-
actualDataRows = [...result];
|
|
219
|
-
for (const [key, value] of Object.entries(result)) {
|
|
220
|
-
if (isNaN(parseInt(key))) { otherParams[key] = value; }
|
|
221
|
-
}
|
|
222
|
-
} else {
|
|
223
|
-
for (const [key, value] of Object.entries(result)) { otherParams[key] = value; }
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
const columnMetadata = otherParams.columns;
|
|
227
|
-
if (Array.isArray(columnMetadata) && Array.isArray(actualDataRows) && actualDataRows.length > 0) {
|
|
228
|
-
const sqlBitColumnNames = new Set();
|
|
229
|
-
columnMetadata.forEach((col) => {
|
|
230
|
-
if (col && typeof col.name === "string" && col.dataTypeName === "SQL_BIT") {
|
|
231
|
-
sqlBitColumnNames.add(col.name);
|
|
232
|
-
}
|
|
233
|
-
});
|
|
234
|
-
if (sqlBitColumnNames.size > 0) {
|
|
235
|
-
actualDataRows.forEach((row) => {
|
|
236
|
-
if (typeof row === "object" && row !== null) {
|
|
237
|
-
for (const columnName of sqlBitColumnNames) {
|
|
238
|
-
if (row.hasOwnProperty(columnName)) {
|
|
239
|
-
const value = row[columnName];
|
|
240
|
-
if (value === "1" || value === 1) { row[columnName] = true; }
|
|
241
|
-
else if (value === "0" || value === 0) { row[columnName] = false; }
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
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
|
-
if (Object.keys(otherParams).length) { newMsg.odbc = otherParams; }
|
|
257
|
-
return newMsg;
|
|
139
|
+
// ... (Pas de changement dans cette section)
|
|
258
140
|
};
|
|
259
141
|
|
|
260
142
|
// =================================================================
|
|
@@ -268,11 +150,18 @@ module.exports = function (RED) {
|
|
|
268
150
|
let chunk = [];
|
|
269
151
|
|
|
270
152
|
try {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
153
|
+
if (!this.poolNode) {
|
|
154
|
+
throw new Error("Le noeud de configuration ODBC n'est pas disponible.");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// CORRECTION : Obtenir la chaîne de connexion depuis le noeud de config
|
|
158
|
+
const connectionString = this.poolNode._buildConnectionString();
|
|
159
|
+
if (!connectionString) {
|
|
160
|
+
throw new Error("Impossible de construire une chaîne de connexion valide.");
|
|
274
161
|
}
|
|
275
|
-
|
|
162
|
+
|
|
163
|
+
// CORRECTION : Appeler .cursor() comme une fonction de haut niveau du module odbc
|
|
164
|
+
cursor = await odbcModule.cursor(connectionString, queryString, queryParams);
|
|
276
165
|
|
|
277
166
|
this.status({ fill: "blue", shape: "dot", text: "streaming rows..." });
|
|
278
167
|
let row = await cursor.fetch();
|
|
@@ -303,7 +192,6 @@ module.exports = function (RED) {
|
|
|
303
192
|
this.status({ fill: "green", shape: "dot", text: `success (${rowCount} rows)` });
|
|
304
193
|
if(done) done();
|
|
305
194
|
} catch(err) {
|
|
306
|
-
// L'erreur sera transmise à l'appelant (runQuery)
|
|
307
195
|
throw err;
|
|
308
196
|
}
|
|
309
197
|
finally {
|
|
@@ -312,6 +200,9 @@ module.exports = function (RED) {
|
|
|
312
200
|
};
|
|
313
201
|
|
|
314
202
|
this.runQuery = async (msg, send, done) => {
|
|
203
|
+
// La logique de cette fonction (séparation streaming / non-streaming) reste la même
|
|
204
|
+
// que dans la correction précédente et est toujours valide.
|
|
205
|
+
// ... (Le code de runQuery de la réponse précédente est ici)
|
|
315
206
|
let isPreparedStatement = false;
|
|
316
207
|
let connectionFromPool = null;
|
|
317
208
|
|
|
@@ -348,13 +239,9 @@ module.exports = function (RED) {
|
|
|
348
239
|
currentQueryString = mustache.render(currentQueryString, msg);
|
|
349
240
|
}
|
|
350
241
|
|
|
351
|
-
// CORRECTION : Logique séparée pour streaming et non-streaming
|
|
352
242
|
if (this.config.streaming) {
|
|
353
|
-
// Le mode Streaming appelle directement la fonction corrigée
|
|
354
243
|
await this.executeStreamQuery(currentQueryString, currentQueryParams, msg, send, done);
|
|
355
|
-
|
|
356
244
|
} else {
|
|
357
|
-
// Le mode non-streaming utilise la logique de connexion/retry existante
|
|
358
245
|
const executeNonQuery = async (conn) => {
|
|
359
246
|
const processedMsg = await this.executeQueryAndProcess(conn, currentQueryString, currentQueryParams, isPreparedStatement, msg);
|
|
360
247
|
this.status({ fill: "green", shape: "dot", text: "success" });
|
|
@@ -417,14 +304,23 @@ module.exports = function (RED) {
|
|
|
417
304
|
if (done) { done(finalError); } else { this.error(finalError, msg); }
|
|
418
305
|
}
|
|
419
306
|
};
|
|
420
|
-
|
|
307
|
+
|
|
421
308
|
// =================================================================
|
|
422
309
|
// FIN DE LA SECTION CORRIGÉE
|
|
423
310
|
// =================================================================
|
|
424
|
-
|
|
311
|
+
|
|
425
312
|
this.checkPool = async function (msg, send, done) {
|
|
426
313
|
try {
|
|
427
314
|
if (!this.poolNode) { throw new Error("ODBC Config node not properly configured."); }
|
|
315
|
+
|
|
316
|
+
// Pour le mode streaming, on n'a pas besoin d'attendre l'initialisation du *pool*,
|
|
317
|
+
// mais on a besoin du noeud de config.
|
|
318
|
+
if (this.config.streaming) {
|
|
319
|
+
await this.runQuery(msg, send, done);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// La logique ci-dessous ne s'applique qu'au mode non-streaming
|
|
428
324
|
if (this.poolNode.connecting) {
|
|
429
325
|
this.warn("Waiting for connection pool to initialize...");
|
|
430
326
|
this.status({ fill: "yellow", shape: "ring", text: "Waiting for pool" });
|
|
@@ -436,9 +332,8 @@ module.exports = function (RED) {
|
|
|
436
332
|
}, 1000);
|
|
437
333
|
return;
|
|
438
334
|
}
|
|
439
|
-
// S'assure que le pool est créé avant toute requête, y compris en streaming
|
|
440
335
|
if (!this.poolNode.pool) {
|
|
441
|
-
await this.poolNode.connect().then(c => c.close());
|
|
336
|
+
await this.poolNode.connect().then(c => c.close());
|
|
442
337
|
}
|
|
443
338
|
await this.runQuery(msg, send, done);
|
|
444
339
|
} catch (err) {
|
|
@@ -449,34 +344,11 @@ module.exports = function (RED) {
|
|
|
449
344
|
};
|
|
450
345
|
|
|
451
346
|
this.on("input", async (msg, send, done) => {
|
|
452
|
-
|
|
453
|
-
if (this.poolNode && this.poolNode.config.retryOnMsg) {
|
|
454
|
-
this.log("New message received, overriding retry timer and attempting query now.");
|
|
455
|
-
clearTimeout(this.retryTimer);
|
|
456
|
-
this.retryTimer = null;
|
|
457
|
-
this.isAwaitingRetry = false;
|
|
458
|
-
} else {
|
|
459
|
-
this.warn("Node is in a retry-wait state. New message ignored as per configuration.");
|
|
460
|
-
if (done) done();
|
|
461
|
-
return;
|
|
462
|
-
}
|
|
463
|
-
}
|
|
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
|
-
}
|
|
347
|
+
// ... (Pas de changement dans cette section)
|
|
471
348
|
});
|
|
472
349
|
|
|
473
350
|
this.on("close", async (done) => {
|
|
474
|
-
|
|
475
|
-
clearTimeout(this.retryTimer);
|
|
476
|
-
this.log("Cleared pending retry timer on node close/redeploy.");
|
|
477
|
-
}
|
|
478
|
-
this.status({});
|
|
479
|
-
done();
|
|
351
|
+
// ... (Pas de changement dans cette section)
|
|
480
352
|
});
|
|
481
353
|
|
|
482
354
|
if (this.poolNode) {
|
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.2",
|
|
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",
|