@bkmj/node-red-contrib-odbcmj 2.1.4 → 2.2.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/README.md +1 -1
- package/odbc.html +1 -1
- package/odbc.js +91 -110
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -105,4 +105,4 @@ When streaming is active, each output message will contain:
|
|
|
105
105
|
- A `msg.odbc_stream` object with metadata for tracking progress:
|
|
106
106
|
- `index`: The starting index of the current chunk (e.g., 0, 100, 200...).
|
|
107
107
|
- `count`: The number of rows in the current chunk.
|
|
108
|
-
- `complete`: A boolean that is `true` only on the very last message
|
|
108
|
+
- `complete`: A boolean that is `true` only on the very last message, and `false` otherwise. The last payload will always be an empty array. This is useful for triggering a downstream action once all rows have been processed.
|
package/odbc.html
CHANGED
|
@@ -430,5 +430,5 @@ When streaming is active, each output message will contain:
|
|
|
430
430
|
- A `msg.odbc_stream` object with metadata for tracking progress:
|
|
431
431
|
- `index`: The starting index of the current chunk (e.g., 0, 100, 200...).
|
|
432
432
|
- `count`: The number of rows in the current chunk.
|
|
433
|
-
- `complete`: A boolean that is `true` only on the very last message of the stream, useful for triggering a final action.
|
|
433
|
+
- `complete`: A boolean that is `true` only on the very last message of the stream, useful for triggering a final action. The last payload will always be an empty array.
|
|
434
434
|
</script>
|
package/odbc.js
CHANGED
|
@@ -123,15 +123,14 @@ module.exports = function (RED) {
|
|
|
123
123
|
});
|
|
124
124
|
|
|
125
125
|
|
|
126
|
-
// --- ODBC Query Node ---
|
|
126
|
+
// --- ODBC Query Node ---
|
|
127
127
|
function odbc(config) {
|
|
128
128
|
RED.nodes.createNode(this, config);
|
|
129
129
|
this.config = config;
|
|
130
130
|
this.poolNode = RED.nodes.getNode(this.config.connection);
|
|
131
131
|
this.name = this.config.name;
|
|
132
|
-
// La logique de retry complexe est temporairement retirée pour stabiliser le noeud.
|
|
133
132
|
|
|
134
|
-
//
|
|
133
|
+
// ... (enhanceError et executeQueryAndProcess restent inchangés)
|
|
135
134
|
this.enhanceError = (error, query, params, defaultMessage = "Query error") => {
|
|
136
135
|
const queryContext = (() => {
|
|
137
136
|
let s = "";
|
|
@@ -153,11 +152,9 @@ module.exports = function (RED) {
|
|
|
153
152
|
if (params) finalError.params = params;
|
|
154
153
|
return finalError;
|
|
155
154
|
};
|
|
156
|
-
|
|
157
|
-
// Cette fonction reste presque inchangée, elle est maintenant appelée depuis on("input")
|
|
155
|
+
|
|
158
156
|
this.executeQueryAndProcess = async (dbConnection, queryString, queryParams, msg) => {
|
|
159
157
|
const result = await dbConnection.query(queryString, queryParams);
|
|
160
|
-
|
|
161
158
|
if (typeof result === "undefined") { throw new Error("Query returned undefined."); }
|
|
162
159
|
const newMsg = RED.util.cloneMessage(msg);
|
|
163
160
|
const otherParams = {};
|
|
@@ -199,144 +196,128 @@ module.exports = function (RED) {
|
|
|
199
196
|
return newMsg;
|
|
200
197
|
};
|
|
201
198
|
|
|
202
|
-
// =================================================================
|
|
203
|
-
// NOUVELLE IMPLEMENTATION DU STREAMING
|
|
204
|
-
// =================================================================
|
|
205
199
|
this.executeStreamQuery = async (dbConnection, queryString, queryParams, msg, send) => {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
// 1. VÉRIFIER D'ABORD LA FIN DU FLUX
|
|
223
|
-
if (!rows || rows.length === 0) {
|
|
224
|
-
// Le flux de la base de données est terminé.
|
|
225
|
-
// Le contenu actuel de `chunk` est le tout dernier lot.
|
|
226
|
-
if (chunk.length > 0) {
|
|
227
|
-
const newMsg = RED.util.cloneMessage(msg);
|
|
228
|
-
objPath.set(newMsg, this.config.outputObj, chunk);
|
|
229
|
-
// C'est le message final, donc `complete` est TRUE.
|
|
230
|
-
newMsg.odbc_stream = { index: rowCount - chunk.length, count: chunk.length, complete: true };
|
|
231
|
-
send(newMsg);
|
|
232
|
-
} else if (rowCount === 0) {
|
|
233
|
-
// Gérer le cas où la requête ne retourne aucune ligne.
|
|
234
|
-
const newMsg = RED.util.cloneMessage(msg);
|
|
235
|
-
objPath.set(newMsg, this.config.outputObj, []);
|
|
236
|
-
newMsg.odbc_stream = { index: 0, count: 0, complete: true };
|
|
237
|
-
send(newMsg);
|
|
200
|
+
const chunkSize = parseInt(this.config.streamChunkSize) || 1;
|
|
201
|
+
const fetchSize = chunkSize > 100 ? 100 : chunkSize; // Optimisation du fetch
|
|
202
|
+
let cursor;
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
cursor = await dbConnection.query(queryString, queryParams, { cursor: true, fetchSize: fetchSize });
|
|
206
|
+
this.status({ fill: "blue", shape: "dot", text: "streaming rows..." });
|
|
207
|
+
|
|
208
|
+
let rowCount = 0;
|
|
209
|
+
let chunk = [];
|
|
210
|
+
|
|
211
|
+
while (true) {
|
|
212
|
+
const rows = await cursor.fetch();
|
|
213
|
+
if (!rows || rows.length === 0) {
|
|
214
|
+
break;
|
|
238
215
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
// Ce lot n'est pas le dernier, donc `complete` est FALSE.
|
|
251
|
-
newMsg.odbc_stream = { index: rowCount - chunk.length, count: chunk.length, complete: false };
|
|
252
|
-
send(newMsg);
|
|
253
|
-
// Vider le lot pour le prochain remplissage.
|
|
254
|
-
chunk = [];
|
|
216
|
+
|
|
217
|
+
for (const row of rows) {
|
|
218
|
+
rowCount++;
|
|
219
|
+
chunk.push(row);
|
|
220
|
+
if (chunk.length >= chunkSize) {
|
|
221
|
+
const newMsg = RED.util.cloneMessage(msg);
|
|
222
|
+
objPath.set(newMsg, this.config.outputObj, chunk);
|
|
223
|
+
newMsg.odbc_stream = { index: rowCount - chunk.length, count: chunk.length, complete: false };
|
|
224
|
+
send(newMsg);
|
|
225
|
+
chunk = [];
|
|
226
|
+
}
|
|
255
227
|
}
|
|
256
228
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
229
|
+
|
|
230
|
+
if (chunk.length > 0) {
|
|
231
|
+
const newMsg = RED.util.cloneMessage(msg);
|
|
232
|
+
objPath.set(newMsg, this.config.outputObj, chunk);
|
|
233
|
+
newMsg.odbc_stream = { index: rowCount - chunk.length, count: chunk.length, complete: false };
|
|
234
|
+
send(newMsg);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const finalMsg = RED.util.cloneMessage(msg);
|
|
238
|
+
objPath.set(finalMsg, this.config.outputObj, []);
|
|
239
|
+
finalMsg.odbc_stream = { index: rowCount, count: 0, complete: true };
|
|
240
|
+
send(finalMsg);
|
|
241
|
+
|
|
242
|
+
this.status({ fill: "green", shape: "dot", text: `success (${rowCount} rows)` });
|
|
243
|
+
|
|
244
|
+
} finally {
|
|
245
|
+
if (cursor) {
|
|
246
|
+
await cursor.close();
|
|
247
|
+
}
|
|
264
248
|
}
|
|
265
|
-
}
|
|
266
|
-
};
|
|
249
|
+
};
|
|
267
250
|
|
|
268
|
-
// =================================================================
|
|
269
|
-
// NOUVELLE LOGIQUE D'ENTREE UNIFIEE
|
|
270
|
-
// =================================================================
|
|
271
251
|
this.on("input", async (msg, send, done) => {
|
|
272
252
|
if (!this.poolNode) {
|
|
273
|
-
|
|
274
|
-
this.status({ fill: "red", shape: "ring", text: "No config node" });
|
|
275
|
-
done(err);
|
|
276
|
-
return;
|
|
253
|
+
return done(new Error("ODBC Config node not properly configured."));
|
|
277
254
|
}
|
|
278
255
|
|
|
279
|
-
|
|
280
|
-
try {
|
|
281
|
-
this.status({ fill: "blue", shape: "dot", text: "preparing..." });
|
|
256
|
+
const execute = async (connection) => {
|
|
282
257
|
this.config.outputObj = this.config.outputObj || "payload";
|
|
283
|
-
|
|
284
|
-
// Obtenir la requête et les paramètres
|
|
258
|
+
|
|
285
259
|
const querySourceType = this.config.querySourceType || 'msg';
|
|
286
260
|
const querySource = this.config.querySource || 'query';
|
|
287
261
|
const paramsSourceType = this.config.paramsSourceType || 'msg';
|
|
288
262
|
const paramsSource = this.config.paramsSource || 'parameters';
|
|
289
|
-
|
|
290
|
-
const currentQueryParams = await new Promise((resolve) => {
|
|
291
|
-
RED.util.evaluateNodeProperty(paramsSource, paramsSourceType, this, msg, (err, value) => resolve(err ? undefined : value));
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
let currentQueryString = await new Promise((resolve) => {
|
|
295
|
-
RED.util.evaluateNodeProperty(querySource, querySourceType, this, msg, (err, value) => resolve(err ? undefined : (value || this.config.query || "")));
|
|
296
|
-
});
|
|
297
263
|
|
|
298
|
-
|
|
264
|
+
const params = await new Promise(resolve => RED.util.evaluateNodeProperty(paramsSource, paramsSourceType, this, msg, (err, val) => resolve(err ? undefined : val)));
|
|
265
|
+
let query = await new Promise(resolve => RED.util.evaluateNodeProperty(querySource, querySourceType, this, msg, (err, val) => resolve(err ? undefined : (val || this.config.query || ""))));
|
|
299
266
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
267
|
+
if (!query) throw new Error("No query to execute");
|
|
268
|
+
|
|
269
|
+
const isPreparedStatement = params || (query && query.includes("?"));
|
|
270
|
+
if (!isPreparedStatement && query) {
|
|
271
|
+
query = mustache.render(query, msg);
|
|
303
272
|
}
|
|
304
273
|
|
|
305
|
-
// Obtenir une connexion du pool
|
|
306
|
-
this.status({ fill: "yellow", shape: "dot", text: "connecting..." });
|
|
307
|
-
connection = await this.poolNode.connect();
|
|
308
274
|
this.status({ fill: "blue", shape: "dot", text: "executing..." });
|
|
309
|
-
|
|
310
275
|
if (this.config.streaming) {
|
|
311
|
-
await this.executeStreamQuery(connection,
|
|
276
|
+
await this.executeStreamQuery(connection, query, params, msg, send);
|
|
312
277
|
} else {
|
|
313
|
-
const newMsg = await this.executeQueryAndProcess(connection,
|
|
278
|
+
const newMsg = await this.executeQueryAndProcess(connection, query, params, msg);
|
|
314
279
|
this.status({ fill: "green", shape: "dot", text: "success" });
|
|
315
280
|
send(newMsg);
|
|
316
281
|
}
|
|
317
|
-
|
|
318
|
-
// Si tout s'est bien passé, on appelle done() sans erreur
|
|
319
|
-
done();
|
|
320
|
-
|
|
321
|
-
} catch (err) {
|
|
322
|
-
const finalError = this.enhanceError(err, null, null, "Query Execution Failed");
|
|
323
|
-
this.status({ fill: "red", shape: "ring", text: "query error" });
|
|
324
|
-
done(finalError); // On passe l'erreur à done() pour que Node-RED la gère
|
|
282
|
+
};
|
|
325
283
|
|
|
326
|
-
|
|
327
|
-
|
|
284
|
+
let connectionFromPool;
|
|
285
|
+
try {
|
|
286
|
+
this.status({ fill: "yellow", shape: "dot", text: "connecting..." });
|
|
287
|
+
connectionFromPool = await this.poolNode.connect();
|
|
288
|
+
await execute(connectionFromPool);
|
|
289
|
+
return done();
|
|
290
|
+
} catch (poolError) {
|
|
291
|
+
this.warn(`First attempt with pooled connection failed: ${poolError.message}`);
|
|
292
|
+
if (this.poolNode.config.retryFreshConnection) {
|
|
293
|
+
this.warn("Attempting retry with a fresh connection.");
|
|
294
|
+
this.status({ fill: "yellow", shape: "dot", text: "Retrying (fresh)..." });
|
|
295
|
+
let freshConnection;
|
|
328
296
|
try {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
this.
|
|
297
|
+
const freshConnectConfig = this.poolNode.getFreshConnectionConfig();
|
|
298
|
+
freshConnection = await odbcModule.connect(freshConnectConfig);
|
|
299
|
+
this.log("Fresh connection established for retry.");
|
|
300
|
+
await execute(freshConnection);
|
|
301
|
+
this.log("Query successful with fresh connection. Resetting pool.");
|
|
302
|
+
await this.poolNode.resetPool();
|
|
303
|
+
return done();
|
|
304
|
+
} catch (freshError) {
|
|
305
|
+
this.status({ fill: "red", shape: "ring", text: "retry failed" });
|
|
306
|
+
return done(this.enhanceError(freshError, null, null, "Retry with fresh connection also failed"));
|
|
307
|
+
} finally {
|
|
308
|
+
if (freshConnection) await freshConnection.close();
|
|
332
309
|
}
|
|
310
|
+
} else {
|
|
311
|
+
this.status({ fill: "red", shape: "ring", text: "query error" });
|
|
312
|
+
return done(this.enhanceError(poolError));
|
|
333
313
|
}
|
|
314
|
+
} finally {
|
|
315
|
+
if (connectionFromPool) await connectionFromPool.close();
|
|
334
316
|
}
|
|
335
317
|
});
|
|
336
318
|
|
|
337
319
|
this.on("close", (done) => {
|
|
338
320
|
this.status({});
|
|
339
|
-
// La logique de fermeture du pool est déjà dans le noeud de config
|
|
340
321
|
done();
|
|
341
322
|
});
|
|
342
323
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bkmj/node-red-contrib-odbcmj",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.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",
|