@bkmj/node-red-contrib-odbcmj 2.1.4 → 2.2.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/README.md +1 -1
- package/odbc.html +40 -10
- package/odbc.js +149 -118
- 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
|
@@ -71,12 +71,36 @@
|
|
|
71
71
|
|
|
72
72
|
$('#node-config-test-connection').on('click', function() {
|
|
73
73
|
var button = $(this);
|
|
74
|
+
var connectionMode = $("#node-config-input-connectionMode").val();
|
|
75
|
+
|
|
76
|
+
if (connectionMode === 'structured') {
|
|
77
|
+
var server = $("#node-config-input-server").val().trim();
|
|
78
|
+
if (!server) {
|
|
79
|
+
RED.notify("Le champ 'Server' est requis pour le test.", {type: "warning", timeout: 3000});
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
var connStr = $("#node-config-input-connectionString").val().trim();
|
|
84
|
+
if (!connStr) {
|
|
85
|
+
RED.notify("La chaîne de connexion est requise pour le test.", {type: "warning", timeout: 3000});
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
// Nouvelle validation :
|
|
89
|
+
// Elle est invalide SEULEMENT si elle ne contient PAS de DSN ET qu'elle ne respecte PAS l'ancien critère pour les chaînes DSN-less.
|
|
90
|
+
var isDsnString = /DSN=[^;]+/i.test(connStr);
|
|
91
|
+
var isDriverBasedString = /DRIVER=\{.+?\}/i.test(connStr) && /(SERVER|DATABASE|UID|PWD)=/i.test(connStr); // Un peu plus complet
|
|
92
|
+
|
|
93
|
+
if (!isDsnString && !isDriverBasedString) {
|
|
94
|
+
RED.notify("La chaîne de connexion semble invalide ou incomplète (ex: DSN=value; ou DRIVER={...};SERVER=...;).", {type: "warning", timeout: 4000});
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
74
99
|
var originalText = "Test Connection";
|
|
75
100
|
var icon = button.find("i");
|
|
76
101
|
icon.removeClass('fa-bolt').addClass('fa-spinner fa-spin');
|
|
77
102
|
button.text(' Testing...').prop('disabled', true);
|
|
78
103
|
|
|
79
|
-
var connectionMode = $("#node-config-input-connectionMode").val();
|
|
80
104
|
var configData = {
|
|
81
105
|
connectionMode: connectionMode,
|
|
82
106
|
dbType: $("#node-config-input-dbType").val(),
|
|
@@ -272,7 +296,6 @@
|
|
|
272
296
|
outputObj: { value: "payload" },
|
|
273
297
|
streaming: { value: false },
|
|
274
298
|
streamChunkSize: { value: 1, validate: RED.validators.number() },
|
|
275
|
-
// Nouveaux `defaults` pour les Typed Inputs
|
|
276
299
|
querySource: { value: "query", required: false },
|
|
277
300
|
querySourceType: { value: "msg", required: false },
|
|
278
301
|
paramsSource: { value: "parameters", required: false },
|
|
@@ -295,7 +318,6 @@
|
|
|
295
318
|
$(".stream-options").toggle(this.checked);
|
|
296
319
|
}).trigger("change");
|
|
297
320
|
|
|
298
|
-
// Initialisation des Typed Inputs
|
|
299
321
|
$("#node-input-querySource").typedInput({
|
|
300
322
|
default: 'msg',
|
|
301
323
|
typeField: "#node-input-querySourceType",
|
|
@@ -414,7 +436,7 @@ Executes a query against a configured ODBC data source.
|
|
|
414
436
|
### Properties
|
|
415
437
|
|
|
416
438
|
- **Connection**: The `odbc config` node to use.
|
|
417
|
-
- **Query (fallback)**: A static SQL query to run if the "Query
|
|
439
|
+
- **Query (fallback)**: A static SQL query to run if the "Query (fallback)" does not provide one. Can contain Mustache syntax (e.g., `{{{payload.id}}}`).
|
|
418
440
|
- **Result to**: The `msg` property where the query result will be stored. Default: `payload`.
|
|
419
441
|
|
|
420
442
|
### Streaming Results
|
|
@@ -425,10 +447,18 @@ For queries that return a large number of rows, streaming prevents high memory u
|
|
|
425
447
|
- **`Chunk Size`**: The number of rows to include in each output message. A value of `1` means one message will be sent for every single row.
|
|
426
448
|
|
|
427
449
|
#### Streaming Output Format
|
|
428
|
-
When streaming is active,
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
-
|
|
450
|
+
When streaming is active, the node sends messages in sequence:
|
|
451
|
+
|
|
452
|
+
1. **Data Messages**: One or more messages where the payload (or the configured output property) contains an array of rows for the current chunk. For these messages, `msg.odbc_stream.complete` will be **`false`**.
|
|
453
|
+
|
|
454
|
+
2. **Completion Message**: A single, final message indicating the end of the stream. For this message:
|
|
455
|
+
- The payload will be an **empty array `[]`**.
|
|
456
|
+
- `msg.odbc_stream.complete` will be **`true`**.
|
|
457
|
+
|
|
458
|
+
The `msg.odbc_stream` object contains metadata for tracking:
|
|
459
|
+
- `index`: The starting index of the current chunk. For the completion message, this will be the total number of rows processed.
|
|
460
|
+
- `count`: The number of rows in the chunk. This will be `0` for the completion message.
|
|
461
|
+
- `complete`: The boolean flag (`true`/`false`).
|
|
462
|
+
|
|
463
|
+
This pattern ensures you can reliably trigger a final action (like closing a file or calculating an aggregate) only when the message with `complete: true` is received.
|
|
434
464
|
</script>
|
package/odbc.js
CHANGED
|
@@ -12,7 +12,6 @@ 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
|
|
16
15
|
this._buildConnectionString = function() {
|
|
17
16
|
if (this.config.connectionMode === 'structured') {
|
|
18
17
|
if (!this.config.dbType || !this.config.server) {
|
|
@@ -119,19 +118,69 @@ module.exports = function (RED) {
|
|
|
119
118
|
});
|
|
120
119
|
|
|
121
120
|
RED.httpAdmin.post("/odbc_config/:id/test", RED.auth.needsPermission("odbc.write"), async function(req, res) {
|
|
122
|
-
|
|
123
|
-
|
|
121
|
+
const tempConfig = req.body;
|
|
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
|
+
|
|
155
|
+
// ==============================================================
|
|
156
|
+
// LIGNE DE DÉBOGAGE AJOUTÉE
|
|
157
|
+
// ==============================================================
|
|
158
|
+
console.log("[ODBC Test] Attempting to connect with string:", testConnectionString);
|
|
159
|
+
// ==============================================================
|
|
124
160
|
|
|
161
|
+
const connectionOptions = {
|
|
162
|
+
connectionString: testConnectionString,
|
|
163
|
+
loginTimeout: 10
|
|
164
|
+
};
|
|
165
|
+
connection = await odbcModule.connect(connectionOptions);
|
|
166
|
+
res.sendStatus(200);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
console.error("[ODBC Test] Connection failed:", err); // Ajout d'un log d'erreur
|
|
169
|
+
res.status(500).send(err.message || "Erreur inconnue durant le test.");
|
|
170
|
+
} finally {
|
|
171
|
+
if (connection) {
|
|
172
|
+
await connection.close();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
});
|
|
125
176
|
|
|
126
|
-
// --- ODBC Query Node ---
|
|
177
|
+
// --- ODBC Query Node ---
|
|
127
178
|
function odbc(config) {
|
|
128
179
|
RED.nodes.createNode(this, config);
|
|
129
180
|
this.config = config;
|
|
130
181
|
this.poolNode = RED.nodes.getNode(this.config.connection);
|
|
131
182
|
this.name = this.config.name;
|
|
132
|
-
// La logique de retry complexe est temporairement retirée pour stabiliser le noeud.
|
|
133
183
|
|
|
134
|
-
// Cette fonction reste inchangée
|
|
135
184
|
this.enhanceError = (error, query, params, defaultMessage = "Query error") => {
|
|
136
185
|
const queryContext = (() => {
|
|
137
186
|
let s = "";
|
|
@@ -153,11 +202,9 @@ module.exports = function (RED) {
|
|
|
153
202
|
if (params) finalError.params = params;
|
|
154
203
|
return finalError;
|
|
155
204
|
};
|
|
156
|
-
|
|
157
|
-
// Cette fonction reste presque inchangée, elle est maintenant appelée depuis on("input")
|
|
205
|
+
|
|
158
206
|
this.executeQueryAndProcess = async (dbConnection, queryString, queryParams, msg) => {
|
|
159
207
|
const result = await dbConnection.query(queryString, queryParams);
|
|
160
|
-
|
|
161
208
|
if (typeof result === "undefined") { throw new Error("Query returned undefined."); }
|
|
162
209
|
const newMsg = RED.util.cloneMessage(msg);
|
|
163
210
|
const otherParams = {};
|
|
@@ -198,148 +245,132 @@ module.exports = function (RED) {
|
|
|
198
245
|
if (Object.keys(otherParams).length) { newMsg.odbc = otherParams; }
|
|
199
246
|
return newMsg;
|
|
200
247
|
};
|
|
201
|
-
|
|
202
|
-
// =================================================================
|
|
203
|
-
// NOUVELLE IMPLEMENTATION DU STREAMING
|
|
204
|
-
// =================================================================
|
|
248
|
+
|
|
205
249
|
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);
|
|
250
|
+
const chunkSize = parseInt(this.config.streamChunkSize) || 1;
|
|
251
|
+
const fetchSize = chunkSize > 100 ? 100 : chunkSize;
|
|
252
|
+
let cursor;
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
cursor = await dbConnection.query(queryString, queryParams, { cursor: true, fetchSize: fetchSize });
|
|
256
|
+
this.status({ fill: "blue", shape: "dot", text: "streaming rows..." });
|
|
257
|
+
|
|
258
|
+
let rowCount = 0;
|
|
259
|
+
let chunk = [];
|
|
260
|
+
|
|
261
|
+
while (true) {
|
|
262
|
+
const rows = await cursor.fetch();
|
|
263
|
+
if (!rows || rows.length === 0) {
|
|
264
|
+
break;
|
|
238
265
|
}
|
|
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 = [];
|
|
266
|
+
|
|
267
|
+
for (const row of rows) {
|
|
268
|
+
rowCount++;
|
|
269
|
+
chunk.push(row);
|
|
270
|
+
if (chunk.length >= chunkSize) {
|
|
271
|
+
const newMsg = RED.util.cloneMessage(msg);
|
|
272
|
+
objPath.set(newMsg, this.config.outputObj, chunk);
|
|
273
|
+
newMsg.odbc_stream = { index: rowCount - chunk.length, count: chunk.length, complete: false };
|
|
274
|
+
send(newMsg);
|
|
275
|
+
chunk = [];
|
|
276
|
+
}
|
|
255
277
|
}
|
|
256
278
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
279
|
+
|
|
280
|
+
if (chunk.length > 0) {
|
|
281
|
+
const newMsg = RED.util.cloneMessage(msg);
|
|
282
|
+
objPath.set(newMsg, this.config.outputObj, chunk);
|
|
283
|
+
newMsg.odbc_stream = { index: rowCount - chunk.length, count: chunk.length, complete: false };
|
|
284
|
+
send(newMsg);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const finalMsg = RED.util.cloneMessage(msg);
|
|
288
|
+
objPath.set(finalMsg, this.config.outputObj, []);
|
|
289
|
+
finalMsg.odbc_stream = { index: rowCount, count: 0, complete: true };
|
|
290
|
+
send(finalMsg);
|
|
291
|
+
|
|
292
|
+
this.status({ fill: "green", shape: "dot", text: `success (${rowCount} rows)` });
|
|
293
|
+
|
|
294
|
+
} finally {
|
|
295
|
+
if (cursor) {
|
|
296
|
+
await cursor.close();
|
|
297
|
+
}
|
|
264
298
|
}
|
|
265
|
-
}
|
|
266
|
-
};
|
|
299
|
+
};
|
|
267
300
|
|
|
268
|
-
// =================================================================
|
|
269
|
-
// NOUVELLE LOGIQUE D'ENTREE UNIFIEE
|
|
270
|
-
// =================================================================
|
|
271
301
|
this.on("input", async (msg, send, done) => {
|
|
272
302
|
if (!this.poolNode) {
|
|
273
|
-
|
|
274
|
-
this.status({ fill: "red", shape: "ring", text: "No config node" });
|
|
275
|
-
done(err);
|
|
276
|
-
return;
|
|
303
|
+
return done(new Error("ODBC Config node not properly configured."));
|
|
277
304
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
try {
|
|
281
|
-
this.status({ fill: "blue", shape: "dot", text: "preparing..." });
|
|
305
|
+
|
|
306
|
+
const execute = async (connection) => {
|
|
282
307
|
this.config.outputObj = this.config.outputObj || "payload";
|
|
283
|
-
|
|
284
|
-
// Obtenir la requête et les paramètres
|
|
308
|
+
|
|
285
309
|
const querySourceType = this.config.querySourceType || 'msg';
|
|
286
310
|
const querySource = this.config.querySource || 'query';
|
|
287
311
|
const paramsSourceType = this.config.paramsSourceType || 'msg';
|
|
288
312
|
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
313
|
|
|
298
|
-
|
|
314
|
+
const params = await new Promise(resolve => RED.util.evaluateNodeProperty(paramsSource, paramsSourceType, this, msg, (err, val) => resolve(err ? undefined : val)));
|
|
315
|
+
let query = await new Promise(resolve => RED.util.evaluateNodeProperty(querySource, querySourceType, this, msg, (err, val) => resolve(err ? undefined : (val || this.config.query || ""))));
|
|
299
316
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
317
|
+
if (!query) throw new Error("No query to execute");
|
|
318
|
+
|
|
319
|
+
const isPreparedStatement = params || (query && query.includes("?"));
|
|
320
|
+
if (!isPreparedStatement && query) {
|
|
321
|
+
query = mustache.render(query, msg);
|
|
303
322
|
}
|
|
304
323
|
|
|
305
|
-
// Obtenir une connexion du pool
|
|
306
|
-
this.status({ fill: "yellow", shape: "dot", text: "connecting..." });
|
|
307
|
-
connection = await this.poolNode.connect();
|
|
308
324
|
this.status({ fill: "blue", shape: "dot", text: "executing..." });
|
|
309
|
-
|
|
310
325
|
if (this.config.streaming) {
|
|
311
|
-
await this.executeStreamQuery(connection,
|
|
326
|
+
await this.executeStreamQuery(connection, query, params, msg, send);
|
|
312
327
|
} else {
|
|
313
|
-
const newMsg = await this.executeQueryAndProcess(connection,
|
|
328
|
+
const newMsg = await this.executeQueryAndProcess(connection, query, params, msg);
|
|
314
329
|
this.status({ fill: "green", shape: "dot", text: "success" });
|
|
315
330
|
send(newMsg);
|
|
316
331
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
done(
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
if (
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
let connectionFromPool;
|
|
335
|
+
try {
|
|
336
|
+
this.status({ fill: "yellow", shape: "dot", text: "connecting..." });
|
|
337
|
+
connectionFromPool = await this.poolNode.connect();
|
|
338
|
+
await execute(connectionFromPool);
|
|
339
|
+
return done();
|
|
340
|
+
} catch (poolError) {
|
|
341
|
+
this.warn(`First attempt with pooled connection failed: ${poolError.message}`);
|
|
342
|
+
if (this.poolNode.config.retryFreshConnection) {
|
|
343
|
+
this.warn("Attempting retry with a fresh connection.");
|
|
344
|
+
this.status({ fill: "yellow", shape: "dot", text: "Retrying (fresh)..." });
|
|
345
|
+
let freshConnection;
|
|
328
346
|
try {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
this.
|
|
347
|
+
const freshConnectConfig = this.poolNode.getFreshConnectionConfig();
|
|
348
|
+
freshConnection = await odbcModule.connect(freshConnectConfig);
|
|
349
|
+
this.log("Fresh connection established for retry.");
|
|
350
|
+
await execute(freshConnection);
|
|
351
|
+
this.log("Query successful with fresh connection. Resetting pool.");
|
|
352
|
+
await this.poolNode.resetPool();
|
|
353
|
+
return done();
|
|
354
|
+
} catch (freshError) {
|
|
355
|
+
this.status({ fill: "red", shape: "ring", text: "retry failed" });
|
|
356
|
+
return done(this.enhanceError(freshError, null, null, "Retry with fresh connection also failed"));
|
|
357
|
+
} finally {
|
|
358
|
+
if (freshConnection) await freshConnection.close();
|
|
332
359
|
}
|
|
360
|
+
} else {
|
|
361
|
+
this.status({ fill: "red", shape: "ring", text: "query error" });
|
|
362
|
+
return done(this.enhanceError(poolError));
|
|
333
363
|
}
|
|
364
|
+
} finally {
|
|
365
|
+
if (connectionFromPool) await connectionFromPool.close();
|
|
334
366
|
}
|
|
335
367
|
});
|
|
336
368
|
|
|
337
369
|
this.on("close", (done) => {
|
|
338
370
|
this.status({});
|
|
339
|
-
// La logique de fermeture du pool est déjà dans le noeud de config
|
|
340
371
|
done();
|
|
341
372
|
});
|
|
342
|
-
|
|
373
|
+
|
|
343
374
|
if (this.poolNode) {
|
|
344
375
|
this.status({ fill: "green", shape: "dot", text: "ready" });
|
|
345
376
|
} else {
|
|
@@ -347,6 +378,6 @@ module.exports = function (RED) {
|
|
|
347
378
|
this.warn("ODBC Config node not found or not deployed.");
|
|
348
379
|
}
|
|
349
380
|
}
|
|
350
|
-
|
|
381
|
+
|
|
351
382
|
RED.nodes.registerType("odbc", odbc);
|
|
352
383
|
};
|
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.2.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",
|