@bkmj/node-red-contrib-odbcmj 1.6.6 → 1.7.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 +18 -113
- package/odbc.html +226 -146
- package/odbc.js +623 -194
- package/package.json +1 -1
package/odbc.js
CHANGED
|
@@ -1,304 +1,733 @@
|
|
|
1
|
-
module.exports = function(RED) {
|
|
2
|
-
const odbcModule = require(
|
|
3
|
-
const mustache = require(
|
|
4
|
-
const objPath = require(
|
|
1
|
+
module.exports = function (RED) {
|
|
2
|
+
const odbcModule = require("odbc");
|
|
3
|
+
const mustache = require("mustache");
|
|
4
|
+
const objPath = require("object-path");
|
|
5
5
|
|
|
6
6
|
// --- ODBC Configuration Node ---
|
|
7
7
|
function poolConfig(config) {
|
|
8
|
-
RED.nodes.createNode(this, config);
|
|
9
|
-
this.config = config; //
|
|
10
|
-
this.pool = null;
|
|
11
|
-
this.connecting = false;
|
|
8
|
+
RED.nodes.createNode(this, config);
|
|
9
|
+
this.config = config; // Contient connectionString, initialSize, retryFreshConnection, etc.
|
|
10
|
+
this.pool = null;
|
|
11
|
+
this.connecting = false;
|
|
12
12
|
|
|
13
|
-
const enableSyntaxChecker = this.config.syntaxtick;
|
|
13
|
+
const enableSyntaxChecker = this.config.syntaxtick;
|
|
14
14
|
const syntax = this.config.syntax;
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
// Garder une copie de la config originale pour la connexion fraîche si besoin
|
|
16
|
+
this.originalConfigForFreshConnection = {
|
|
17
|
+
connectionString: this.config.connectionString,
|
|
18
|
+
connectionTimeout: parseInt(this.config.connectionTimeout) || 0, // Assurer un integer, 0 pour certains drivers signifie pas de timeout spécifique à l'appel connect
|
|
19
|
+
loginTimeout: parseInt(this.config.loginTimeout) || 0,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
delete this.config.syntaxtick; // Ces champs ne sont pas pour odbc.pool directement
|
|
23
|
+
// delete this.config.syntax; // 'syntax' pourrait être utile si odbc.pool l'acceptait
|
|
17
24
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
? new require('node-sql-parser/build/' + syntax).Parser()
|
|
25
|
+
this.parser = enableSyntaxChecker
|
|
26
|
+
? new require("node-sql-parser/build/" + syntax).Parser()
|
|
21
27
|
: null;
|
|
22
28
|
|
|
23
|
-
// Convert numeric config params to integers
|
|
24
29
|
for (const [key, value] of Object.entries(this.config)) {
|
|
25
30
|
if (!isNaN(parseInt(value))) {
|
|
26
31
|
this.config[key] = parseInt(value);
|
|
27
32
|
}
|
|
28
33
|
}
|
|
34
|
+
// 'retryFreshConnection' est déjà dans this.config grâce à la création du noeud
|
|
29
35
|
|
|
30
|
-
// Connect to the database and create a connection pool
|
|
31
36
|
this.connect = async () => {
|
|
37
|
+
// Si le pool n'existe pas (ou a été reset), on le crée
|
|
32
38
|
if (!this.pool) {
|
|
39
|
+
this.connecting = true;
|
|
40
|
+
this.status({
|
|
41
|
+
fill: "yellow",
|
|
42
|
+
shape: "dot",
|
|
43
|
+
text: "Pool init...",
|
|
44
|
+
});
|
|
33
45
|
try {
|
|
34
|
-
|
|
46
|
+
// Utiliser une copie de la config sans retryFreshConnection pour odbc.pool
|
|
47
|
+
const poolParams = { ...this.config };
|
|
48
|
+
delete poolParams.retryFreshConnection;
|
|
49
|
+
delete poolParams.syntax; // Retiré car non utilisé par odbc.pool
|
|
50
|
+
|
|
51
|
+
this.pool = await odbcModule.pool(poolParams);
|
|
35
52
|
this.connecting = false;
|
|
53
|
+
this.status({
|
|
54
|
+
fill: "green",
|
|
55
|
+
shape: "dot",
|
|
56
|
+
text: "Pool ready",
|
|
57
|
+
});
|
|
58
|
+
this.log("Connection pool initialized successfully.");
|
|
36
59
|
} catch (error) {
|
|
37
|
-
|
|
38
|
-
this.error(
|
|
39
|
-
|
|
40
|
-
|
|
60
|
+
this.connecting = false;
|
|
61
|
+
this.error(
|
|
62
|
+
`Error creating connection pool: ${error}`,
|
|
63
|
+
error
|
|
64
|
+
);
|
|
65
|
+
this.status({
|
|
66
|
+
fill: "red",
|
|
67
|
+
shape: "ring",
|
|
68
|
+
text: "Pool error",
|
|
69
|
+
});
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Quoi qu'il arrive, on demande une connexion au pool (qui pourrait être fraîchement créé)
|
|
74
|
+
try {
|
|
75
|
+
return await this.pool.connect();
|
|
76
|
+
} catch (poolConnectError) {
|
|
77
|
+
this.error(
|
|
78
|
+
`Error connecting to pool: ${poolConnectError}`,
|
|
79
|
+
poolConnectError
|
|
80
|
+
);
|
|
81
|
+
this.status({
|
|
82
|
+
fill: "red",
|
|
83
|
+
shape: "ring",
|
|
84
|
+
text: "Pool connect err",
|
|
85
|
+
});
|
|
86
|
+
throw poolConnectError;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// --- NOUVELLE MÉTHODE: resetPool ---
|
|
91
|
+
this.resetPool = async () => {
|
|
92
|
+
if (this.pool) {
|
|
93
|
+
this.log("Resetting connection pool.");
|
|
94
|
+
this.status({
|
|
95
|
+
fill: "yellow",
|
|
96
|
+
shape: "ring",
|
|
97
|
+
text: "Resetting pool...",
|
|
98
|
+
});
|
|
99
|
+
try {
|
|
100
|
+
await this.pool.close();
|
|
101
|
+
this.log("Connection pool closed successfully for reset.");
|
|
102
|
+
} catch (closeError) {
|
|
103
|
+
this.error(
|
|
104
|
+
`Error closing pool during reset: ${closeError}`,
|
|
105
|
+
closeError
|
|
106
|
+
);
|
|
107
|
+
// Continuer pour nullifier le pool même en cas d'erreur de fermeture
|
|
108
|
+
} finally {
|
|
109
|
+
this.pool = null;
|
|
110
|
+
this.connecting = false; // Permet à this.connect de recréer le pool
|
|
111
|
+
// Le statut sera mis à jour par la prochaine tentative de connexion via this.connect()
|
|
41
112
|
}
|
|
113
|
+
} else {
|
|
114
|
+
this.log("Pool reset requested, but no active pool to reset.");
|
|
42
115
|
}
|
|
43
|
-
return await this.pool.connect();
|
|
44
116
|
};
|
|
45
117
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
118
|
+
this.on("close", async (removed, done) => {
|
|
119
|
+
// 'removed' est true si le noeud est supprimé, false si juste redéployé.
|
|
120
|
+
// Nous voulons fermer le pool dans les deux cas si nous en sommes propriétaires.
|
|
121
|
+
this.log("Closing ODBC config node. Attempting to close pool.");
|
|
122
|
+
if (this.pool) {
|
|
49
123
|
try {
|
|
50
124
|
await this.pool.close();
|
|
125
|
+
this.log(
|
|
126
|
+
"Connection pool closed successfully on node close."
|
|
127
|
+
);
|
|
128
|
+
this.pool = null;
|
|
51
129
|
} catch (error) {
|
|
52
|
-
|
|
53
|
-
|
|
130
|
+
this.error(
|
|
131
|
+
`Error closing connection pool on node close: ${error}`,
|
|
132
|
+
error
|
|
133
|
+
);
|
|
54
134
|
}
|
|
55
135
|
}
|
|
56
136
|
done();
|
|
57
137
|
});
|
|
58
138
|
}
|
|
59
139
|
|
|
60
|
-
RED.nodes.registerType(
|
|
140
|
+
RED.nodes.registerType("odbc config", poolConfig);
|
|
61
141
|
|
|
62
142
|
// --- ODBC Query Node ---
|
|
63
143
|
function odbc(config) {
|
|
64
144
|
RED.nodes.createNode(this, config);
|
|
65
145
|
this.config = config;
|
|
66
|
-
this.poolNode = RED.nodes.getNode(this.config.connection);
|
|
146
|
+
this.poolNode = RED.nodes.getNode(this.config.connection); // C'est le noeud 'odbc config'
|
|
67
147
|
this.name = this.config.name;
|
|
68
148
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (
|
|
79
|
-
|
|
149
|
+
// --- NOUVELLE MÉTHODE: Helper pour améliorer les messages d'erreur ---
|
|
150
|
+
this.enhanceError = (
|
|
151
|
+
error,
|
|
152
|
+
query,
|
|
153
|
+
params,
|
|
154
|
+
defaultMessage = "Query error"
|
|
155
|
+
) => {
|
|
156
|
+
const queryContext = (() => {
|
|
157
|
+
let s = "";
|
|
158
|
+
if (query || params) {
|
|
159
|
+
s += " {";
|
|
160
|
+
if (query)
|
|
161
|
+
s += `"query": '${query.substring(0, 100)}${
|
|
162
|
+
query.length > 100 ? "..." : ""
|
|
163
|
+
}'`; // Tronquer les longues requêtes
|
|
164
|
+
if (params) s += `, "params": '${JSON.stringify(params)}'`;
|
|
165
|
+
s += "}";
|
|
166
|
+
return s;
|
|
80
167
|
}
|
|
168
|
+
return "";
|
|
169
|
+
})();
|
|
170
|
+
|
|
171
|
+
let finalError;
|
|
172
|
+
if (typeof error === "object" && error !== null && error.message) {
|
|
173
|
+
finalError = error;
|
|
174
|
+
} else if (typeof error === "string") {
|
|
175
|
+
finalError = new Error(error);
|
|
176
|
+
} else {
|
|
177
|
+
finalError = new Error(defaultMessage);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
finalError.message = `${finalError.message}${queryContext}`;
|
|
181
|
+
if (query) finalError.query = query;
|
|
182
|
+
if (params) finalError.params = params;
|
|
183
|
+
|
|
184
|
+
return finalError;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// --- NOUVELLE MÉTHODE: Helper pour exécuter la requête et traiter le résultat ---
|
|
188
|
+
// Prend une connexion (du pool ou fraîche) en argument
|
|
189
|
+
this.executeQueryAndProcess = async (
|
|
190
|
+
dbConnection,
|
|
191
|
+
queryString,
|
|
192
|
+
queryParams,
|
|
193
|
+
isPreparedStatement,
|
|
194
|
+
msg
|
|
195
|
+
) => {
|
|
196
|
+
let result;
|
|
197
|
+
// Exécution de la requête
|
|
198
|
+
if (isPreparedStatement) {
|
|
199
|
+
const stmt = await dbConnection.createStatement();
|
|
200
|
+
try {
|
|
201
|
+
await stmt.prepare(queryString);
|
|
202
|
+
await stmt.bind(queryParams); // queryParams vient de msg.parameters
|
|
203
|
+
result = await stmt.execute();
|
|
204
|
+
} finally {
|
|
205
|
+
// Assurer la fermeture du statement même en cas d'erreur de execute()
|
|
206
|
+
// stmt.close() peut être synchrone ou asynchrone selon les drivers/versions de odbc
|
|
207
|
+
if (stmt && typeof stmt.close === "function") {
|
|
208
|
+
try {
|
|
209
|
+
await stmt.close();
|
|
210
|
+
} catch (stmtCloseError) {
|
|
211
|
+
this.warn(
|
|
212
|
+
`Error closing statement: ${stmtCloseError}`
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
} else {
|
|
218
|
+
result = await dbConnection.query(queryString, queryParams); // queryParams ici aussi
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (typeof result === "undefined") {
|
|
222
|
+
// Certains drivers/erreurs pourraient retourner undefined
|
|
223
|
+
throw new Error(
|
|
224
|
+
"Query returned undefined. Check for errors or empty results."
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Traitement du résultat (SQL_BIT, otherParams, etc.)
|
|
229
|
+
// Créer une copie du message pour éviter de modifier l'original en cas de retry
|
|
230
|
+
const newMsg = RED.util.cloneMessage(msg);
|
|
231
|
+
|
|
232
|
+
const otherParams = {};
|
|
233
|
+
let actualDataRows = [];
|
|
234
|
+
|
|
235
|
+
if (result !== null && typeof result === "object") {
|
|
236
|
+
// Si result est un array, il contient les lignes.
|
|
237
|
+
// Les propriétés non-numériques (comme .columns, .count) sont extraites.
|
|
238
|
+
if (Array.isArray(result)) {
|
|
239
|
+
actualDataRows = [...result]; // Copie des lignes
|
|
240
|
+
for (const [key, value] of Object.entries(result)) {
|
|
241
|
+
if (isNaN(parseInt(key))) {
|
|
242
|
+
otherParams[key] = value;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
} else {
|
|
246
|
+
// Si result est un objet mais pas un array (ex: { count: 0, columns: [...] })
|
|
247
|
+
// Il n'y a pas de "lignes" au sens array, mais otherParams peut contenir des metadonnées.
|
|
248
|
+
for (const [key, value] of Object.entries(result)) {
|
|
249
|
+
otherParams[key] = value;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const columnMetadata = otherParams.columns;
|
|
255
|
+
if (
|
|
256
|
+
Array.isArray(columnMetadata) &&
|
|
257
|
+
Array.isArray(actualDataRows) &&
|
|
258
|
+
actualDataRows.length > 0
|
|
259
|
+
) {
|
|
260
|
+
const sqlBitColumnNames = new Set();
|
|
261
|
+
columnMetadata.forEach((col) => {
|
|
262
|
+
if (
|
|
263
|
+
col &&
|
|
264
|
+
typeof col.name === "string" &&
|
|
265
|
+
col.dataTypeName === "SQL_BIT"
|
|
266
|
+
) {
|
|
267
|
+
sqlBitColumnNames.add(col.name);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
if (sqlBitColumnNames.size > 0) {
|
|
272
|
+
actualDataRows.forEach((row) => {
|
|
273
|
+
if (typeof row === "object" && row !== null) {
|
|
274
|
+
for (const columnName of sqlBitColumnNames) {
|
|
275
|
+
if (row.hasOwnProperty(columnName)) {
|
|
276
|
+
const value = row[columnName];
|
|
277
|
+
if (value === "1" || value === 1) {
|
|
278
|
+
row[columnName] = true;
|
|
279
|
+
} else if (value === "0" || value === 0) {
|
|
280
|
+
row[columnName] = false;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
objPath.set(newMsg, this.config.outputObj, actualDataRows);
|
|
290
|
+
|
|
291
|
+
if (this.poolNode?.parser && queryString) {
|
|
292
|
+
try {
|
|
293
|
+
// Utiliser structuredClone est une bonne pratique pour éviter les modifications par référence
|
|
294
|
+
newMsg.parsedQuery = this.poolNode.parser.astify(
|
|
295
|
+
structuredClone(queryString)
|
|
296
|
+
);
|
|
297
|
+
} catch (syntaxError) {
|
|
298
|
+
this.warn(
|
|
299
|
+
`Could not parse query for parsedQuery output: ${syntaxError}`
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
81
303
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
304
|
+
if (Object.keys(otherParams).length) {
|
|
305
|
+
newMsg.odbc = otherParams;
|
|
306
|
+
}
|
|
307
|
+
return newMsg;
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
this.runQuery = async function (msg, send, done) {
|
|
311
|
+
let currentQueryString = this.config.query || ""; // Initialiser avec la config du noeud
|
|
312
|
+
let currentQueryParams = msg.parameters; // Peut être undefined
|
|
313
|
+
let isPreparedStatement = false;
|
|
314
|
+
let connectionFromPool = null; // Pour s'assurer de sa fermeture
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
this.status({
|
|
318
|
+
fill: "blue",
|
|
319
|
+
shape: "dot",
|
|
320
|
+
text: "querying...",
|
|
321
|
+
});
|
|
322
|
+
this.config.outputObj =
|
|
323
|
+
msg?.output || this.config?.outputObj || "payload";
|
|
324
|
+
|
|
325
|
+
// --- Construction de la requête (adapté de l'original) ---
|
|
326
|
+
// Déterminer si c'est un prepared statement AVANT le render mustache
|
|
327
|
+
isPreparedStatement =
|
|
328
|
+
currentQueryParams ||
|
|
329
|
+
(currentQueryString && currentQueryString.includes("?"));
|
|
330
|
+
|
|
331
|
+
if (!isPreparedStatement && currentQueryString) {
|
|
332
|
+
// Mustache rendering uniquement si ce n'est pas un PS avec des '?'
|
|
333
|
+
// Et si currentQueryString est défini
|
|
334
|
+
for (const parsed of mustache.parse(currentQueryString)) {
|
|
86
335
|
if (parsed[0] === "name" || parsed[0] === "&") {
|
|
87
336
|
if (!objPath.has(msg, parsed[1])) {
|
|
88
|
-
this.warn(
|
|
337
|
+
this.warn(
|
|
338
|
+
`Mustache parameter "${parsed[1]}" is absent and will render to undefined`
|
|
339
|
+
);
|
|
89
340
|
}
|
|
90
341
|
}
|
|
91
342
|
}
|
|
92
|
-
|
|
343
|
+
currentQueryString = mustache.render(
|
|
344
|
+
currentQueryString,
|
|
345
|
+
msg
|
|
346
|
+
);
|
|
93
347
|
}
|
|
94
348
|
|
|
95
|
-
// Handle cases where the query is provided in the message
|
|
96
349
|
if (msg?.query) {
|
|
97
|
-
|
|
98
|
-
|
|
350
|
+
// Priorité à msg.query
|
|
351
|
+
if (
|
|
352
|
+
currentQueryString &&
|
|
353
|
+
currentQueryString !== this.config.query
|
|
354
|
+
) {
|
|
355
|
+
this.log(
|
|
356
|
+
"Query from node config (possibly mustache rendered) was overwritten by msg.query."
|
|
357
|
+
);
|
|
358
|
+
} else if (this.config.query) {
|
|
359
|
+
this.log(
|
|
360
|
+
"Query from node config was overwritten by msg.query."
|
|
361
|
+
);
|
|
99
362
|
}
|
|
100
|
-
|
|
363
|
+
currentQueryString = msg.query;
|
|
101
364
|
} else if (msg?.payload) {
|
|
102
|
-
|
|
365
|
+
// Ensuite msg.payload.query ou msg.payload (si string)
|
|
366
|
+
if (typeof msg.payload === "string") {
|
|
103
367
|
try {
|
|
104
368
|
const payloadJson = JSON.parse(msg.payload);
|
|
105
|
-
if (
|
|
106
|
-
|
|
369
|
+
if (
|
|
370
|
+
payloadJson?.query &&
|
|
371
|
+
typeof payloadJson.query === "string"
|
|
372
|
+
) {
|
|
373
|
+
currentQueryString = payloadJson.query;
|
|
107
374
|
}
|
|
108
|
-
} catch (err) {
|
|
109
|
-
|
|
110
|
-
|
|
375
|
+
} catch (err) {
|
|
376
|
+
/* Pas un JSON ou pas de query, on ignore */
|
|
377
|
+
}
|
|
378
|
+
} else if (
|
|
379
|
+
msg.payload?.query &&
|
|
380
|
+
typeof msg.payload.query === "string"
|
|
381
|
+
) {
|
|
382
|
+
currentQueryString = msg.payload.query;
|
|
111
383
|
}
|
|
112
384
|
}
|
|
113
385
|
|
|
114
|
-
if (!
|
|
386
|
+
if (!currentQueryString) {
|
|
115
387
|
throw new Error("No query to execute");
|
|
116
388
|
}
|
|
117
389
|
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
} else {
|
|
123
|
-
// If parameters are provided as an object, extract parameter names from the query
|
|
124
|
-
// and create an ordered array of values for the prepared statement.
|
|
125
|
-
if (typeof msg.parameters === 'object' && !Array.isArray(msg.parameters)) {
|
|
126
|
-
const paramNames = this.queryString.match(/\(([^)]*)\)/)[1].split(',').map(el => el.trim());
|
|
390
|
+
// Re-vérifier isPreparedStatement si query a changé, et valider les paramètres
|
|
391
|
+
isPreparedStatement =
|
|
392
|
+
currentQueryParams ||
|
|
393
|
+
(currentQueryString && currentQueryString.includes("?"));
|
|
127
394
|
|
|
128
|
-
|
|
129
|
-
|
|
395
|
+
if (isPreparedStatement) {
|
|
396
|
+
if (!currentQueryParams) {
|
|
397
|
+
throw new Error(
|
|
398
|
+
"Prepared statement ('?' in query) requires msg.parameters to be provided."
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
if (
|
|
402
|
+
typeof currentQueryParams === "object" &&
|
|
403
|
+
!Array.isArray(currentQueryParams)
|
|
404
|
+
) {
|
|
405
|
+
// Tentative de mapper un objet à un array basé sur les noms dans la query (simplifié)
|
|
406
|
+
// Cette logique est complexe et sujette à erreur si les noms ne matchent pas parfaitement.
|
|
407
|
+
// La documentation originale suggère un mapping auto, mais c'est risqué.
|
|
408
|
+
// Pour l'instant, on se fie à l'ordre si c'est un objet.
|
|
409
|
+
// Une solution plus robuste serait d'analyser la query pour les noms de paramètres si le driver le supporte.
|
|
410
|
+
// Ou d'exiger un array pour les '?'
|
|
411
|
+
// Pour l'instant, on va assumer que si c'est un objet, l'utilisateur a une logique spécifique ou le driver le gère
|
|
412
|
+
// La logique originale de mappage par nom de paramètre dans `()` est retirée pour simplification,
|
|
413
|
+
// car elle est très spécifique et peut ne pas être standard.
|
|
414
|
+
// this.warn("msg.parameters is an object for a '?' prepared statement. Order of properties will be used. For explicit order, use an array.");
|
|
415
|
+
// currentQueryParams = Object.values(currentQueryParams); // Ceci est une supposition sur l'ordre.
|
|
416
|
+
// La meilleure approche est de demander un Array pour les '?'
|
|
417
|
+
if (
|
|
418
|
+
(currentQueryString.match(/\?/g) || []).length !==
|
|
419
|
+
Object.keys(currentQueryParams).length &&
|
|
420
|
+
(currentQueryString.match(/\?/g) || []).length !==
|
|
421
|
+
currentQueryParams.length
|
|
422
|
+
) {
|
|
423
|
+
// La logique originale pour mapper les noms de paramètres pour les '?' était `this.queryString.match(/\(([^)]*)\)/)[1].split(",").map((el) => el.trim());`
|
|
424
|
+
// Ceci n'est pas standard pour les `?`. Normalement, `?` attend un array.
|
|
425
|
+
// On va laisser le driver/odbc gérer si `msg.parameters` est un objet.
|
|
426
|
+
// Mais on va vérifier le nombre de `?` vs la taille de `msg.parameters` si c'est un array.
|
|
427
|
+
// Si c'est un objet, on ne peut pas facilement vérifier le nombre.
|
|
130
428
|
}
|
|
131
429
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
throw new Error(
|
|
430
|
+
if (
|
|
431
|
+
!Array.isArray(currentQueryParams) &&
|
|
432
|
+
typeof currentQueryParams !== "object"
|
|
433
|
+
) {
|
|
434
|
+
// Doit être array ou objet
|
|
435
|
+
throw new Error(
|
|
436
|
+
"msg.parameters must be an array or an object for prepared statements."
|
|
437
|
+
);
|
|
138
438
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
439
|
+
if (
|
|
440
|
+
Array.isArray(currentQueryParams) &&
|
|
441
|
+
(currentQueryString.match(/\?/g) || []).length !==
|
|
442
|
+
currentQueryParams.length
|
|
443
|
+
) {
|
|
444
|
+
throw new Error(
|
|
445
|
+
"Incorrect number of parameters in msg.parameters array for '?' placeholders."
|
|
446
|
+
);
|
|
147
447
|
}
|
|
148
448
|
}
|
|
149
449
|
|
|
150
|
-
//
|
|
450
|
+
// Validation du champ de sortie
|
|
151
451
|
if (!this.config.outputObj) {
|
|
152
|
-
throw new Error(
|
|
452
|
+
throw new Error(
|
|
453
|
+
"Invalid output object definition (outputObj is empty)"
|
|
454
|
+
);
|
|
153
455
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
this.config.outputObj.
|
|
159
|
-
|
|
456
|
+
const reg = new RegExp(
|
|
457
|
+
'^((?![,;:`\\[\\]{}+=()!"$%?&*|<>\\/^¨`\\s]).)*$'
|
|
458
|
+
);
|
|
459
|
+
if (
|
|
460
|
+
!this.config.outputObj.match(reg) ||
|
|
461
|
+
this.config.outputObj.startsWith(".") ||
|
|
462
|
+
this.config.outputObj.endsWith(".")
|
|
463
|
+
) {
|
|
464
|
+
throw new Error(
|
|
465
|
+
`Invalid output field name: ${this.config.outputObj}`
|
|
466
|
+
);
|
|
160
467
|
}
|
|
161
468
|
|
|
162
|
-
// ---
|
|
469
|
+
// --- Première tentative avec une connexion du pool ---
|
|
470
|
+
let firstAttemptError = null;
|
|
163
471
|
try {
|
|
164
|
-
|
|
165
|
-
if (!
|
|
166
|
-
|
|
472
|
+
connectionFromPool = await this.poolNode.connect();
|
|
473
|
+
if (!connectionFromPool) {
|
|
474
|
+
// Devrait être géré par poolNode.connect() qui throw une erreur
|
|
475
|
+
throw new Error(
|
|
476
|
+
"Failed to get connection from pool (returned null)"
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
this.status({
|
|
480
|
+
fill: "blue",
|
|
481
|
+
shape: "dot",
|
|
482
|
+
text: "Pool conn OK. Executing...",
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
const processedMsg = await this.executeQueryAndProcess(
|
|
486
|
+
connectionFromPool,
|
|
487
|
+
currentQueryString,
|
|
488
|
+
currentQueryParams,
|
|
489
|
+
isPreparedStatement,
|
|
490
|
+
msg
|
|
491
|
+
);
|
|
492
|
+
this.status({
|
|
493
|
+
fill: "green",
|
|
494
|
+
shape: "dot",
|
|
495
|
+
text: "success",
|
|
496
|
+
});
|
|
497
|
+
send(processedMsg);
|
|
498
|
+
if (done) done();
|
|
499
|
+
return; // Succès à la première tentative
|
|
500
|
+
} catch (err) {
|
|
501
|
+
firstAttemptError = this.enhanceError(
|
|
502
|
+
err,
|
|
503
|
+
currentQueryString,
|
|
504
|
+
currentQueryParams,
|
|
505
|
+
"Query failed with pooled connection"
|
|
506
|
+
);
|
|
507
|
+
this.warn(
|
|
508
|
+
`First attempt failed: ${firstAttemptError.message}`
|
|
509
|
+
);
|
|
510
|
+
// Ne pas remonter l'erreur tout de suite, on va peut-être retenter
|
|
511
|
+
} finally {
|
|
512
|
+
if (connectionFromPool) {
|
|
513
|
+
try {
|
|
514
|
+
await connectionFromPool.close(); // Toujours fermer/remettre la connexion au pool
|
|
515
|
+
} catch (closeErr) {
|
|
516
|
+
this.warn(
|
|
517
|
+
`Error closing pooled connection: ${closeErr}`
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
connectionFromPool = null;
|
|
167
521
|
}
|
|
168
|
-
} catch (error) {
|
|
169
|
-
// Handle connection errors (e.g., log the error, set node status)
|
|
170
|
-
this.error(`Error getting connection: ${error}`);
|
|
171
|
-
this.status({ fill: "red", shape: "ring", text: "Connection error" });
|
|
172
|
-
throw error; // Re-throw to prevent further execution
|
|
173
522
|
}
|
|
174
523
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if (
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
524
|
+
// --- Si la première tentative a échoué (firstAttemptError est défini) ---
|
|
525
|
+
if (firstAttemptError) {
|
|
526
|
+
if (
|
|
527
|
+
this.poolNode &&
|
|
528
|
+
this.poolNode.config.retryFreshConnection
|
|
529
|
+
) {
|
|
530
|
+
this.log("Attempting retry with a fresh connection.");
|
|
531
|
+
this.status({
|
|
532
|
+
fill: "yellow",
|
|
533
|
+
shape: "dot",
|
|
534
|
+
text: "Retrying (fresh)...",
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
let freshConnection = null;
|
|
538
|
+
try {
|
|
539
|
+
// Utiliser les paramètres de connexion originaux stockés dans poolNode
|
|
540
|
+
const freshConnectConfig =
|
|
541
|
+
this.poolNode.originalConfigForFreshConnection;
|
|
542
|
+
if (
|
|
543
|
+
!freshConnectConfig ||
|
|
544
|
+
!freshConnectConfig.connectionString
|
|
545
|
+
) {
|
|
546
|
+
throw new Error(
|
|
547
|
+
"Fresh connection configuration is missing in poolNode."
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
freshConnection = await odbcModule.connect(
|
|
551
|
+
freshConnectConfig
|
|
552
|
+
);
|
|
553
|
+
this.log("Fresh connection established for retry.");
|
|
554
|
+
|
|
555
|
+
const processedFreshMsg =
|
|
556
|
+
await this.executeQueryAndProcess(
|
|
557
|
+
freshConnection,
|
|
558
|
+
currentQueryString,
|
|
559
|
+
currentQueryParams,
|
|
560
|
+
isPreparedStatement,
|
|
561
|
+
msg
|
|
562
|
+
);
|
|
563
|
+
|
|
564
|
+
this.log(
|
|
565
|
+
"Query successful with fresh connection. Resetting pool."
|
|
566
|
+
);
|
|
567
|
+
this.status({
|
|
568
|
+
fill: "green",
|
|
569
|
+
shape: "dot",
|
|
570
|
+
text: "Success (fresh)",
|
|
571
|
+
});
|
|
572
|
+
send(processedFreshMsg);
|
|
573
|
+
|
|
574
|
+
if (this.poolNode.resetPool) {
|
|
575
|
+
await this.poolNode.resetPool(); // Demander au pool de se réinitialiser
|
|
576
|
+
} else {
|
|
577
|
+
this.warn(
|
|
578
|
+
"poolNode.resetPool is not available. Pool cannot be reset automatically."
|
|
579
|
+
);
|
|
580
|
+
}
|
|
192
581
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
582
|
+
if (done) done();
|
|
583
|
+
return; // Succès à la seconde tentative
|
|
584
|
+
} catch (freshError) {
|
|
585
|
+
this.warn(
|
|
586
|
+
`Retry with fresh connection also failed: ${freshError.message}`
|
|
587
|
+
);
|
|
588
|
+
// L'erreur finale sera celle de la tentative fraîche
|
|
589
|
+
throw this.enhanceError(
|
|
590
|
+
freshError,
|
|
591
|
+
currentQueryString,
|
|
592
|
+
currentQueryParams,
|
|
593
|
+
"Query failed on fresh connection retry"
|
|
594
|
+
);
|
|
595
|
+
} finally {
|
|
596
|
+
if (freshConnection) {
|
|
597
|
+
try {
|
|
598
|
+
await freshConnection.close();
|
|
599
|
+
this.log("Fresh connection closed.");
|
|
600
|
+
} catch (closeFreshErr) {
|
|
601
|
+
this.warn(
|
|
602
|
+
`Error closing fresh connection: ${closeFreshErr}`
|
|
603
|
+
);
|
|
604
|
+
}
|
|
200
605
|
}
|
|
201
606
|
}
|
|
202
|
-
objPath.set(msg, this.config.outputObj, result);
|
|
203
|
-
if (this.parseSql) {
|
|
204
|
-
msg.parsedQuery = this.parseSql;
|
|
205
|
-
}
|
|
206
|
-
if (Object.keys(otherParams).length) {
|
|
207
|
-
msg.odbc = otherParams;
|
|
208
|
-
}
|
|
209
|
-
this.status({ fill: 'green', shape: 'dot', text: 'success' });
|
|
210
|
-
send(msg);
|
|
211
607
|
} else {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
} catch (error) {
|
|
215
|
-
const str = (() =>{
|
|
216
|
-
let str = ''
|
|
217
|
-
if(this?.queryString || this?.parameters){
|
|
218
|
-
str += " {"
|
|
219
|
-
if(this?.queryString) str += `"query":'${this.queryString}'`;
|
|
220
|
-
if(msg?.parameters) str += `, "params":'${msg.parameters}'`;
|
|
221
|
-
str += "}"
|
|
222
|
-
return str;
|
|
223
|
-
}
|
|
224
|
-
})()
|
|
225
|
-
if(typeof error == 'object'){
|
|
226
|
-
// Enhance the error object with query information
|
|
227
|
-
|
|
228
|
-
if(this?.queryString) error.query = this.queryString;
|
|
229
|
-
if(this?.parameters) error.params = msg.parameters;
|
|
230
|
-
|
|
231
|
-
if(error?.message){
|
|
232
|
-
error.message += str;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
else if (typeof error == 'string'){
|
|
236
|
-
error += str;
|
|
608
|
+
// retryFreshConnection n'est pas activé, donc on lance l'erreur de la première tentative
|
|
609
|
+
throw firstAttemptError;
|
|
237
610
|
}
|
|
238
|
-
// Handle query errors
|
|
239
|
-
this.status({ fill: "red", shape: "ring", text: "Query error" });
|
|
240
|
-
throw error; // Re-throw to trigger the outer catch block
|
|
241
|
-
} finally {
|
|
242
|
-
await this.connection.close();
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (done) {
|
|
246
|
-
done();
|
|
247
611
|
}
|
|
248
612
|
} catch (err) {
|
|
249
|
-
|
|
613
|
+
// Catch global pour runQuery
|
|
614
|
+
// Assurer que err est bien un objet Error
|
|
615
|
+
const finalError =
|
|
616
|
+
err instanceof Error ? err : new Error(String(err));
|
|
617
|
+
|
|
618
|
+
this.status({
|
|
619
|
+
fill: "red",
|
|
620
|
+
shape: "ring",
|
|
621
|
+
text:
|
|
622
|
+
finalError.message && finalError.message.length < 30
|
|
623
|
+
? finalError.message.substring(0, 29) + "..."
|
|
624
|
+
: "query error",
|
|
625
|
+
});
|
|
626
|
+
|
|
250
627
|
if (done) {
|
|
251
|
-
done(
|
|
628
|
+
done(finalError); // Passer l'erreur au callback done de Node-RED
|
|
252
629
|
} else {
|
|
253
|
-
this.error(
|
|
630
|
+
this.error(finalError, msg); // Utiliser this.error pour logguer l'erreur correctement
|
|
254
631
|
}
|
|
255
632
|
}
|
|
256
|
-
};
|
|
633
|
+
}; // Fin de runQuery
|
|
257
634
|
|
|
258
|
-
|
|
259
|
-
this.checkPool = async function(msg, send, done) {
|
|
635
|
+
this.checkPool = async function (msg, send, done) {
|
|
260
636
|
try {
|
|
637
|
+
if (!this.poolNode) {
|
|
638
|
+
throw new Error(
|
|
639
|
+
"ODBC Connection Configuration node is not properly configured or deployed."
|
|
640
|
+
);
|
|
641
|
+
}
|
|
261
642
|
if (this.poolNode.connecting) {
|
|
262
|
-
|
|
263
|
-
this.
|
|
643
|
+
// Si le pool est en cours d'initialisation
|
|
644
|
+
this.warn("Waiting for connection pool to initialize...");
|
|
645
|
+
this.status({
|
|
646
|
+
fill: "yellow",
|
|
647
|
+
shape: "ring",
|
|
648
|
+
text: "Waiting for pool",
|
|
649
|
+
});
|
|
264
650
|
setTimeout(() => {
|
|
265
|
-
this.checkPool(msg, send, done)
|
|
266
|
-
|
|
651
|
+
this.checkPool(msg, send, done).catch((err) => {
|
|
652
|
+
// Gérer l'erreur de la tentative retardée si elle échoue aussi.
|
|
653
|
+
this.status({
|
|
654
|
+
fill: "red",
|
|
655
|
+
shape: "dot",
|
|
656
|
+
text: "Pool wait failed",
|
|
657
|
+
});
|
|
658
|
+
if (done) {
|
|
659
|
+
done(err);
|
|
660
|
+
} else {
|
|
661
|
+
this.error(err, msg);
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
}, 1000); // Réessayer après 1 seconde
|
|
267
665
|
return;
|
|
268
666
|
}
|
|
269
667
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
668
|
+
// Si le pool n'est pas encore initialisé (ex: premier message après déploiement),
|
|
669
|
+
// poolNode.connect() va le faire.
|
|
670
|
+
// La logique de this.poolNode.connecting doit être gérée DANS poolNode.connect()
|
|
273
671
|
|
|
274
672
|
await this.runQuery(msg, send, done);
|
|
275
673
|
} catch (err) {
|
|
276
|
-
|
|
674
|
+
// Catch pour checkPool (erreurs avant même d'appeler runQuery, ou erreurs non gérées par runQuery)
|
|
675
|
+
const finalError =
|
|
676
|
+
err instanceof Error ? err : new Error(String(err));
|
|
677
|
+
this.status({
|
|
678
|
+
fill: "red",
|
|
679
|
+
shape: "dot",
|
|
680
|
+
text:
|
|
681
|
+
finalError.message && finalError.message.length < 30
|
|
682
|
+
? finalError.message.substring(0, 29) + "..."
|
|
683
|
+
: "Op failed",
|
|
684
|
+
});
|
|
277
685
|
if (done) {
|
|
278
|
-
done(
|
|
686
|
+
done(finalError);
|
|
279
687
|
} else {
|
|
280
|
-
this.error(
|
|
688
|
+
this.error(finalError, msg);
|
|
281
689
|
}
|
|
282
690
|
}
|
|
283
691
|
};
|
|
284
692
|
|
|
285
|
-
this.on(
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
693
|
+
this.on("input", async (msg, send, done) => {
|
|
694
|
+
// Envelopper l'appel à checkPool dans un try-catch au cas où checkPool lui-même aurait une erreur synchrone non gérée
|
|
695
|
+
try {
|
|
696
|
+
await this.checkPool(msg, send, done);
|
|
697
|
+
} catch (error) {
|
|
698
|
+
const finalError =
|
|
699
|
+
error instanceof Error ? error : new Error(String(error));
|
|
700
|
+
this.status({
|
|
701
|
+
fill: "red",
|
|
702
|
+
shape: "ring",
|
|
703
|
+
text: "Input error",
|
|
704
|
+
});
|
|
705
|
+
if (done) {
|
|
706
|
+
done(finalError);
|
|
707
|
+
} else {
|
|
708
|
+
this.error(finalError, msg);
|
|
295
709
|
}
|
|
296
710
|
}
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
this.on("close", async (done) => {
|
|
714
|
+
// La connexion individuelle (this.connection du code original) est maintenant gérée
|
|
715
|
+
// à l'intérieur de runQuery (connectionFromPool et freshConnection) et fermée là.
|
|
716
|
+
// Il n'y a donc plus de this.connection à fermer ici au niveau du noeud odbc.
|
|
717
|
+
// Le poolNode (config node) gère la fermeture de son pool.
|
|
718
|
+
this.status({}); // Clear status on close/redeploy
|
|
297
719
|
done();
|
|
298
720
|
});
|
|
299
721
|
|
|
300
|
-
this.
|
|
722
|
+
if (this.poolNode) {
|
|
723
|
+
this.status({ fill: "green", shape: "dot", text: "ready" });
|
|
724
|
+
} else {
|
|
725
|
+
this.status({ fill: "red", shape: "ring", text: "No config node" });
|
|
726
|
+
this.warn(
|
|
727
|
+
"ODBC Config node not found or not deployed. Please configure and deploy the ODBC connection config node."
|
|
728
|
+
);
|
|
729
|
+
}
|
|
301
730
|
}
|
|
302
731
|
|
|
303
732
|
RED.nodes.registerType("odbc", odbc);
|
|
304
|
-
};
|
|
733
|
+
};
|