@bkmj/node-red-contrib-odbcmj 2.1.3 → 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 +65 -77
- 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
|
@@ -122,16 +122,15 @@ module.exports = function (RED) {
|
|
|
122
122
|
// ... (Pas de changement dans cette section)
|
|
123
123
|
});
|
|
124
124
|
|
|
125
|
-
|
|
126
|
-
// --- ODBC Query Node ---
|
|
125
|
+
|
|
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,29 +196,24 @@ 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
200
|
const chunkSize = parseInt(this.config.streamChunkSize) || 1;
|
|
207
|
-
const fetchSize = chunkSize >
|
|
201
|
+
const fetchSize = chunkSize > 100 ? 100 : chunkSize; // Optimisation du fetch
|
|
208
202
|
let cursor;
|
|
209
|
-
|
|
203
|
+
|
|
210
204
|
try {
|
|
211
|
-
// LA BONNE METHODE !
|
|
212
205
|
cursor = await dbConnection.query(queryString, queryParams, { cursor: true, fetchSize: fetchSize });
|
|
213
|
-
|
|
214
206
|
this.status({ fill: "blue", shape: "dot", text: "streaming rows..." });
|
|
215
|
-
|
|
207
|
+
|
|
216
208
|
let rowCount = 0;
|
|
217
209
|
let chunk = [];
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
210
|
+
|
|
211
|
+
while (true) {
|
|
212
|
+
const rows = await cursor.fetch();
|
|
213
|
+
if (!rows || rows.length === 0) {
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
|
|
225
217
|
for (const row of rows) {
|
|
226
218
|
rowCount++;
|
|
227
219
|
chunk.push(row);
|
|
@@ -234,23 +226,21 @@ module.exports = function (RED) {
|
|
|
234
226
|
}
|
|
235
227
|
}
|
|
236
228
|
}
|
|
237
|
-
|
|
229
|
+
|
|
238
230
|
if (chunk.length > 0) {
|
|
239
231
|
const newMsg = RED.util.cloneMessage(msg);
|
|
240
232
|
objPath.set(newMsg, this.config.outputObj, chunk);
|
|
241
|
-
newMsg.odbc_stream = { index: rowCount - chunk.length, count: chunk.length, complete:
|
|
242
|
-
send(newMsg);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (rowCount === 0) {
|
|
246
|
-
const newMsg = RED.util.cloneMessage(msg);
|
|
247
|
-
objPath.set(newMsg, this.config.outputObj, []);
|
|
248
|
-
newMsg.odbc_stream = { index: 0, count: 0, complete: true };
|
|
233
|
+
newMsg.odbc_stream = { index: rowCount - chunk.length, count: chunk.length, complete: false };
|
|
249
234
|
send(newMsg);
|
|
250
235
|
}
|
|
251
|
-
|
|
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
|
+
|
|
252
242
|
this.status({ fill: "green", shape: "dot", text: `success (${rowCount} rows)` });
|
|
253
|
-
|
|
243
|
+
|
|
254
244
|
} finally {
|
|
255
245
|
if (cursor) {
|
|
256
246
|
await cursor.close();
|
|
@@ -258,78 +248,76 @@ module.exports = function (RED) {
|
|
|
258
248
|
}
|
|
259
249
|
};
|
|
260
250
|
|
|
261
|
-
// =================================================================
|
|
262
|
-
// NOUVELLE LOGIQUE D'ENTREE UNIFIEE
|
|
263
|
-
// =================================================================
|
|
264
251
|
this.on("input", async (msg, send, done) => {
|
|
265
252
|
if (!this.poolNode) {
|
|
266
|
-
|
|
267
|
-
this.status({ fill: "red", shape: "ring", text: "No config node" });
|
|
268
|
-
done(err);
|
|
269
|
-
return;
|
|
253
|
+
return done(new Error("ODBC Config node not properly configured."));
|
|
270
254
|
}
|
|
271
255
|
|
|
272
|
-
|
|
273
|
-
try {
|
|
274
|
-
this.status({ fill: "blue", shape: "dot", text: "preparing..." });
|
|
256
|
+
const execute = async (connection) => {
|
|
275
257
|
this.config.outputObj = this.config.outputObj || "payload";
|
|
276
|
-
|
|
277
|
-
// Obtenir la requête et les paramètres
|
|
258
|
+
|
|
278
259
|
const querySourceType = this.config.querySourceType || 'msg';
|
|
279
260
|
const querySource = this.config.querySource || 'query';
|
|
280
261
|
const paramsSourceType = this.config.paramsSourceType || 'msg';
|
|
281
262
|
const paramsSource = this.config.paramsSource || 'parameters';
|
|
282
|
-
|
|
283
|
-
const currentQueryParams = await new Promise((resolve) => {
|
|
284
|
-
RED.util.evaluateNodeProperty(paramsSource, paramsSourceType, this, msg, (err, value) => resolve(err ? undefined : value));
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
let currentQueryString = await new Promise((resolve) => {
|
|
288
|
-
RED.util.evaluateNodeProperty(querySource, querySourceType, this, msg, (err, value) => resolve(err ? undefined : (value || this.config.query || "")));
|
|
289
|
-
});
|
|
290
263
|
|
|
291
|
-
|
|
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 || ""))));
|
|
292
266
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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);
|
|
296
272
|
}
|
|
297
273
|
|
|
298
|
-
// Obtenir une connexion du pool
|
|
299
|
-
this.status({ fill: "yellow", shape: "dot", text: "connecting..." });
|
|
300
|
-
connection = await this.poolNode.connect();
|
|
301
274
|
this.status({ fill: "blue", shape: "dot", text: "executing..." });
|
|
302
|
-
|
|
303
275
|
if (this.config.streaming) {
|
|
304
|
-
await this.executeStreamQuery(connection,
|
|
276
|
+
await this.executeStreamQuery(connection, query, params, msg, send);
|
|
305
277
|
} else {
|
|
306
|
-
const newMsg = await this.executeQueryAndProcess(connection,
|
|
278
|
+
const newMsg = await this.executeQueryAndProcess(connection, query, params, msg);
|
|
307
279
|
this.status({ fill: "green", shape: "dot", text: "success" });
|
|
308
280
|
send(newMsg);
|
|
309
281
|
}
|
|
310
|
-
|
|
311
|
-
// Si tout s'est bien passé, on appelle done() sans erreur
|
|
312
|
-
done();
|
|
313
|
-
|
|
314
|
-
} catch (err) {
|
|
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
|
|
282
|
+
};
|
|
318
283
|
|
|
319
|
-
|
|
320
|
-
|
|
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;
|
|
321
296
|
try {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
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();
|
|
325
309
|
}
|
|
310
|
+
} else {
|
|
311
|
+
this.status({ fill: "red", shape: "ring", text: "query error" });
|
|
312
|
+
return done(this.enhanceError(poolError));
|
|
326
313
|
}
|
|
314
|
+
} finally {
|
|
315
|
+
if (connectionFromPool) await connectionFromPool.close();
|
|
327
316
|
}
|
|
328
317
|
});
|
|
329
318
|
|
|
330
319
|
this.on("close", (done) => {
|
|
331
320
|
this.status({});
|
|
332
|
-
// La logique de fermeture du pool est déjà dans le noeud de config
|
|
333
321
|
done();
|
|
334
322
|
});
|
|
335
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",
|