@bkmj/node-red-contrib-odbcmj 1.7.0 → 2.0.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 +79 -28
- package/odbc.html +181 -259
- package/odbc.js +259 -536
- package/package.json +26 -7
package/odbc.js
CHANGED
|
@@ -1,114 +1,98 @@
|
|
|
1
1
|
module.exports = function (RED) {
|
|
2
2
|
const odbcModule = require("odbc");
|
|
3
|
-
const mustache = require("mustache");
|
|
4
|
-
const objPath = require("object-path");
|
|
3
|
+
const mustache = require("mustache"); // Utilisé dans runQuery
|
|
4
|
+
const objPath = require("object-path"); // Utilisé pour mustache et le positionnement du résultat
|
|
5
5
|
|
|
6
6
|
// --- ODBC Configuration Node ---
|
|
7
7
|
function poolConfig(config) {
|
|
8
8
|
RED.nodes.createNode(this, config);
|
|
9
|
-
this.config = config;
|
|
9
|
+
this.config = config;
|
|
10
10
|
this.pool = null;
|
|
11
11
|
this.connecting = false;
|
|
12
|
+
|
|
13
|
+
this.credentials = RED.nodes.getCredentials(this.id);
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
15
|
+
this._buildConnectionString = function() {
|
|
16
|
+
if (this.config.connectionMode === 'structured') {
|
|
17
|
+
if (!this.config.dbType || !this.config.server) {
|
|
18
|
+
throw new Error("En mode structuré, le type de base de données et le serveur sont requis.");
|
|
19
|
+
}
|
|
20
|
+
let driver;
|
|
21
|
+
let parts = [];
|
|
22
|
+
switch (this.config.dbType) {
|
|
23
|
+
case 'sqlserver': driver = 'ODBC Driver 17 for SQL Server'; break;
|
|
24
|
+
case 'postgresql': driver = 'PostgreSQL Unicode'; break;
|
|
25
|
+
case 'mysql': driver = 'MySQL ODBC 8.0 Unicode Driver'; break;
|
|
26
|
+
default: driver = this.config.driver || ''; break;
|
|
27
|
+
}
|
|
28
|
+
if(driver) parts.unshift(`DRIVER={${driver}}`);
|
|
29
|
+
parts.push(`SERVER=${this.config.server}`);
|
|
30
|
+
if (this.config.database) parts.push(`DATABASE=${this.config.database}`);
|
|
31
|
+
if (this.config.user) parts.push(`UID=${this.config.user}`);
|
|
32
|
+
if (this.credentials && this.credentials.password) parts.push(`PWD=${this.credentials.password}`);
|
|
33
|
+
return parts.join(';');
|
|
34
|
+
} else {
|
|
35
|
+
let connStr = this.config.connectionString || "";
|
|
36
|
+
if (this.credentials && this.credentials.password && connStr.includes('{{{password}}}')) {
|
|
37
|
+
connStr = connStr.replace('{{{password}}}', this.credentials.password);
|
|
38
|
+
}
|
|
39
|
+
return connStr;
|
|
32
40
|
}
|
|
33
|
-
}
|
|
34
|
-
// 'retryFreshConnection' est déjà dans this.config grâce à la création du noeud
|
|
41
|
+
};
|
|
35
42
|
|
|
36
43
|
this.connect = async () => {
|
|
37
|
-
// Si le pool n'existe pas (ou a été reset), on le crée
|
|
38
44
|
if (!this.pool) {
|
|
39
45
|
this.connecting = true;
|
|
40
|
-
this.status({
|
|
41
|
-
fill: "yellow",
|
|
42
|
-
shape: "dot",
|
|
43
|
-
text: "Pool init...",
|
|
44
|
-
});
|
|
46
|
+
this.status({ fill: "yellow", shape: "dot", text: "Pool init..." });
|
|
45
47
|
try {
|
|
46
|
-
|
|
48
|
+
const finalConnectionString = this._buildConnectionString();
|
|
49
|
+
if (!finalConnectionString) throw new Error("La chaîne de connexion est vide.");
|
|
50
|
+
|
|
47
51
|
const poolParams = { ...this.config };
|
|
48
|
-
|
|
49
|
-
delete poolParams.syntax; // Retiré car non utilisé par odbc.pool
|
|
52
|
+
poolParams.connectionString = finalConnectionString;
|
|
50
53
|
|
|
54
|
+
['retryFreshConnection', 'retryDelay', 'retryOnMsg', 'syntax', 'connectionMode', 'dbType', 'server', 'database', 'user', 'driver'].forEach(k => delete poolParams[k]);
|
|
55
|
+
|
|
51
56
|
this.pool = await odbcModule.pool(poolParams);
|
|
52
57
|
this.connecting = false;
|
|
53
|
-
this.status({
|
|
54
|
-
fill: "green",
|
|
55
|
-
shape: "dot",
|
|
56
|
-
text: "Pool ready",
|
|
57
|
-
});
|
|
58
|
+
this.status({ fill: "green", shape: "dot", text: "Pool ready" });
|
|
58
59
|
this.log("Connection pool initialized successfully.");
|
|
59
60
|
} catch (error) {
|
|
60
61
|
this.connecting = false;
|
|
61
|
-
this.error(
|
|
62
|
-
|
|
63
|
-
error
|
|
64
|
-
);
|
|
65
|
-
this.status({
|
|
66
|
-
fill: "red",
|
|
67
|
-
shape: "ring",
|
|
68
|
-
text: "Pool error",
|
|
69
|
-
});
|
|
62
|
+
this.error(`Error creating connection pool: ${error.message}`, error);
|
|
63
|
+
this.status({ fill: "red", shape: "ring", text: "Pool error" });
|
|
70
64
|
throw error;
|
|
71
65
|
}
|
|
72
66
|
}
|
|
73
|
-
// Quoi qu'il arrive, on demande une connexion au pool (qui pourrait être fraîchement créé)
|
|
74
67
|
try {
|
|
75
68
|
return await this.pool.connect();
|
|
76
69
|
} catch (poolConnectError) {
|
|
77
|
-
this.error(
|
|
78
|
-
|
|
79
|
-
poolConnectError
|
|
80
|
-
);
|
|
81
|
-
this.status({
|
|
82
|
-
fill: "red",
|
|
83
|
-
shape: "ring",
|
|
84
|
-
text: "Pool connect err",
|
|
85
|
-
});
|
|
70
|
+
this.error(`Error connecting to pool: ${poolConnectError}`, poolConnectError);
|
|
71
|
+
this.status({ fill: "red", shape: "ring", text: "Pool connect err" });
|
|
86
72
|
throw poolConnectError;
|
|
87
73
|
}
|
|
88
74
|
};
|
|
89
75
|
|
|
90
|
-
|
|
76
|
+
this.getFreshConnectionConfig = function() {
|
|
77
|
+
return {
|
|
78
|
+
connectionString: this._buildConnectionString(),
|
|
79
|
+
connectionTimeout: parseInt(this.config.connectionTimeout) || 0,
|
|
80
|
+
loginTimeout: parseInt(this.config.loginTimeout) || 0,
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
|
|
91
84
|
this.resetPool = async () => {
|
|
92
|
-
|
|
85
|
+
if (this.pool) {
|
|
93
86
|
this.log("Resetting connection pool.");
|
|
94
|
-
this.status({
|
|
95
|
-
fill: "yellow",
|
|
96
|
-
shape: "ring",
|
|
97
|
-
text: "Resetting pool...",
|
|
98
|
-
});
|
|
87
|
+
this.status({ fill: "yellow", shape: "ring", text: "Resetting pool..." });
|
|
99
88
|
try {
|
|
100
89
|
await this.pool.close();
|
|
101
90
|
this.log("Connection pool closed successfully for reset.");
|
|
102
91
|
} 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
|
|
92
|
+
this.error(`Error closing pool during reset: ${closeError}`, closeError);
|
|
108
93
|
} finally {
|
|
109
94
|
this.pool = null;
|
|
110
|
-
this.connecting = false;
|
|
111
|
-
// Le statut sera mis à jour par la prochaine tentative de connexion via this.connect()
|
|
95
|
+
this.connecting = false;
|
|
112
96
|
}
|
|
113
97
|
} else {
|
|
114
98
|
this.log("Pool reset requested, but no active pool to reset.");
|
|
@@ -116,606 +100,347 @@ module.exports = function (RED) {
|
|
|
116
100
|
};
|
|
117
101
|
|
|
118
102
|
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
103
|
this.log("Closing ODBC config node. Attempting to close pool.");
|
|
122
104
|
if (this.pool) {
|
|
123
105
|
try {
|
|
124
106
|
await this.pool.close();
|
|
125
|
-
this.log(
|
|
126
|
-
"Connection pool closed successfully on node close."
|
|
127
|
-
);
|
|
107
|
+
this.log("Connection pool closed successfully on node close.");
|
|
128
108
|
this.pool = null;
|
|
129
109
|
} catch (error) {
|
|
130
|
-
this.error(
|
|
131
|
-
`Error closing connection pool on node close: ${error}`,
|
|
132
|
-
error
|
|
133
|
-
);
|
|
110
|
+
this.error(`Error closing connection pool on node close: ${error}`, error);
|
|
134
111
|
}
|
|
135
112
|
}
|
|
136
113
|
done();
|
|
137
114
|
});
|
|
138
115
|
}
|
|
139
116
|
|
|
140
|
-
RED.nodes.registerType("odbc config", poolConfig
|
|
117
|
+
RED.nodes.registerType("odbc config", poolConfig, {
|
|
118
|
+
credentials: {
|
|
119
|
+
password: { type: "password" }
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
RED.httpAdmin.post("/odbc_config/:id/test", RED.auth.needsPermission("odbc.write"), async function(req, res) {
|
|
124
|
+
const tempConfig = req.body;
|
|
125
|
+
const tempCredentials = { password: tempConfig.password };
|
|
126
|
+
delete tempConfig.password;
|
|
127
|
+
|
|
128
|
+
const buildTestConnectionString = () => {
|
|
129
|
+
if (tempConfig.connectionMode === 'structured') {
|
|
130
|
+
if (!tempConfig.dbType || !tempConfig.server) return res.status(400).send("Mode structuré : le type de BD et le serveur sont requis.");
|
|
131
|
+
let driver;
|
|
132
|
+
let parts = [];
|
|
133
|
+
switch (tempConfig.dbType) {
|
|
134
|
+
case 'sqlserver': driver = 'ODBC Driver 17 for SQL Server'; break;
|
|
135
|
+
case 'postgresql': driver = 'PostgreSQL Unicode'; break;
|
|
136
|
+
case 'mysql': driver = 'MySQL ODBC 8.0 Unicode Driver'; break;
|
|
137
|
+
default: driver = tempConfig.driver || ''; break;
|
|
138
|
+
}
|
|
139
|
+
if(driver) parts.unshift(`DRIVER={${driver}}`);
|
|
140
|
+
parts.push(`SERVER=${tempConfig.server}`);
|
|
141
|
+
if (tempConfig.database) parts.push(`DATABASE=${tempConfig.database}`);
|
|
142
|
+
if (tempConfig.user) parts.push(`UID=${tempConfig.user}`);
|
|
143
|
+
if (tempCredentials.password) parts.push(`PWD=${tempCredentials.password}`);
|
|
144
|
+
return parts.join(';');
|
|
145
|
+
} else {
|
|
146
|
+
let connStr = tempConfig.connectionString || "";
|
|
147
|
+
if (tempCredentials.password && connStr.includes('{{{password}}}')) {
|
|
148
|
+
connStr = connStr.replace('{{{password}}}', tempCredentials.password);
|
|
149
|
+
}
|
|
150
|
+
return connStr;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
let connection;
|
|
155
|
+
try {
|
|
156
|
+
const testConnectionString = buildTestConnectionString();
|
|
157
|
+
if (!testConnectionString) return res.status(400).send("La chaîne de connexion est vide.");
|
|
158
|
+
connection = await odbcModule.connect(testConnectionString);
|
|
159
|
+
res.sendStatus(200);
|
|
160
|
+
} catch (err) {
|
|
161
|
+
res.status(500).send(err.message || "Erreur inconnue durant le test.");
|
|
162
|
+
} finally {
|
|
163
|
+
if (connection) await connection.close();
|
|
164
|
+
}
|
|
165
|
+
});
|
|
141
166
|
|
|
142
167
|
// --- ODBC Query Node ---
|
|
143
168
|
function odbc(config) {
|
|
144
169
|
RED.nodes.createNode(this, config);
|
|
145
170
|
this.config = config;
|
|
146
|
-
this.poolNode = RED.nodes.getNode(this.config.connection);
|
|
171
|
+
this.poolNode = RED.nodes.getNode(this.config.connection);
|
|
147
172
|
this.name = this.config.name;
|
|
173
|
+
this.isAwaitingRetry = false;
|
|
174
|
+
this.retryTimer = null;
|
|
148
175
|
|
|
149
|
-
|
|
150
|
-
this.enhanceError = (
|
|
151
|
-
error,
|
|
152
|
-
query,
|
|
153
|
-
params,
|
|
154
|
-
defaultMessage = "Query error"
|
|
155
|
-
) => {
|
|
176
|
+
this.enhanceError = (error, query, params, defaultMessage = "Query error") => {
|
|
156
177
|
const queryContext = (() => {
|
|
157
178
|
let s = "";
|
|
158
179
|
if (query || params) {
|
|
159
180
|
s += " {";
|
|
160
|
-
if (query)
|
|
161
|
-
s += `"query": '${query.substring(0, 100)}${
|
|
162
|
-
query.length > 100 ? "..." : ""
|
|
163
|
-
}'`; // Tronquer les longues requêtes
|
|
181
|
+
if (query) s += `"query": '${query.substring(0, 100)}${query.length > 100 ? "..." : ""}'`;
|
|
164
182
|
if (params) s += `, "params": '${JSON.stringify(params)}'`;
|
|
165
183
|
s += "}";
|
|
166
184
|
return s;
|
|
167
185
|
}
|
|
168
186
|
return "";
|
|
169
187
|
})();
|
|
170
|
-
|
|
171
188
|
let finalError;
|
|
172
|
-
if (typeof error === "object" && error !== null && error.message) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
finalError = new Error(error);
|
|
176
|
-
} else {
|
|
177
|
-
finalError = new Error(defaultMessage);
|
|
178
|
-
}
|
|
179
|
-
|
|
189
|
+
if (typeof error === "object" && error !== null && error.message) { finalError = error; }
|
|
190
|
+
else if (typeof error === "string") { finalError = new Error(error); }
|
|
191
|
+
else { finalError = new Error(defaultMessage); }
|
|
180
192
|
finalError.message = `${finalError.message}${queryContext}`;
|
|
181
193
|
if (query) finalError.query = query;
|
|
182
194
|
if (params) finalError.params = params;
|
|
183
|
-
|
|
184
195
|
return finalError;
|
|
185
196
|
};
|
|
186
197
|
|
|
187
|
-
|
|
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
|
-
) => {
|
|
198
|
+
this.executeQueryAndProcess = async (dbConnection, queryString, queryParams, isPreparedStatement, msg) => {
|
|
196
199
|
let result;
|
|
197
|
-
// Exécution de la requête
|
|
198
200
|
if (isPreparedStatement) {
|
|
199
201
|
const stmt = await dbConnection.createStatement();
|
|
200
202
|
try {
|
|
201
203
|
await stmt.prepare(queryString);
|
|
202
|
-
await stmt.bind(queryParams);
|
|
204
|
+
await stmt.bind(queryParams);
|
|
203
205
|
result = await stmt.execute();
|
|
204
206
|
} 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
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
|
-
}
|
|
208
|
+
try { await stmt.close(); } catch (stmtCloseError) { this.warn(`Error closing statement: ${stmtCloseError}`); }
|
|
215
209
|
}
|
|
216
210
|
}
|
|
217
211
|
} else {
|
|
218
|
-
result = await dbConnection.query(queryString, queryParams);
|
|
212
|
+
result = await dbConnection.query(queryString, queryParams);
|
|
219
213
|
}
|
|
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
|
|
214
|
+
if (typeof result === "undefined") { throw new Error("Query returned undefined."); }
|
|
230
215
|
const newMsg = RED.util.cloneMessage(msg);
|
|
231
|
-
|
|
232
216
|
const otherParams = {};
|
|
233
217
|
let actualDataRows = [];
|
|
234
|
-
|
|
235
218
|
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
219
|
if (Array.isArray(result)) {
|
|
239
|
-
actualDataRows = [...result];
|
|
220
|
+
actualDataRows = [...result];
|
|
240
221
|
for (const [key, value] of Object.entries(result)) {
|
|
241
|
-
if (isNaN(parseInt(key))) {
|
|
242
|
-
otherParams[key] = value;
|
|
243
|
-
}
|
|
222
|
+
if (isNaN(parseInt(key))) { otherParams[key] = value; }
|
|
244
223
|
}
|
|
245
224
|
} else {
|
|
246
|
-
|
|
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
|
-
}
|
|
225
|
+
for (const [key, value] of Object.entries(result)) { otherParams[key] = value; }
|
|
251
226
|
}
|
|
252
227
|
}
|
|
253
|
-
|
|
254
228
|
const columnMetadata = otherParams.columns;
|
|
255
|
-
if (
|
|
256
|
-
Array.isArray(columnMetadata) &&
|
|
257
|
-
Array.isArray(actualDataRows) &&
|
|
258
|
-
actualDataRows.length > 0
|
|
259
|
-
) {
|
|
229
|
+
if (Array.isArray(columnMetadata) && Array.isArray(actualDataRows) && actualDataRows.length > 0) {
|
|
260
230
|
const sqlBitColumnNames = new Set();
|
|
261
231
|
columnMetadata.forEach((col) => {
|
|
262
|
-
if (
|
|
263
|
-
col &&
|
|
264
|
-
typeof col.name === "string" &&
|
|
265
|
-
col.dataTypeName === "SQL_BIT"
|
|
266
|
-
) {
|
|
232
|
+
if (col && typeof col.name === "string" && col.dataTypeName === "SQL_BIT") {
|
|
267
233
|
sqlBitColumnNames.add(col.name);
|
|
268
234
|
}
|
|
269
235
|
});
|
|
270
|
-
|
|
271
236
|
if (sqlBitColumnNames.size > 0) {
|
|
272
237
|
actualDataRows.forEach((row) => {
|
|
273
238
|
if (typeof row === "object" && row !== null) {
|
|
274
239
|
for (const columnName of sqlBitColumnNames) {
|
|
275
240
|
if (row.hasOwnProperty(columnName)) {
|
|
276
241
|
const value = row[columnName];
|
|
277
|
-
if (value === "1" || value === 1) {
|
|
278
|
-
|
|
279
|
-
} else if (value === "0" || value === 0) {
|
|
280
|
-
row[columnName] = false;
|
|
281
|
-
}
|
|
242
|
+
if (value === "1" || value === 1) { row[columnName] = true; }
|
|
243
|
+
else if (value === "0" || value === 0) { row[columnName] = false; }
|
|
282
244
|
}
|
|
283
245
|
}
|
|
284
246
|
}
|
|
285
247
|
});
|
|
286
248
|
}
|
|
287
249
|
}
|
|
288
|
-
|
|
289
250
|
objPath.set(newMsg, this.config.outputObj, actualDataRows);
|
|
290
|
-
|
|
291
251
|
if (this.poolNode?.parser && queryString) {
|
|
292
252
|
try {
|
|
293
|
-
|
|
294
|
-
newMsg.parsedQuery = this.poolNode.parser.astify(
|
|
295
|
-
structuredClone(queryString)
|
|
296
|
-
);
|
|
253
|
+
newMsg.parsedQuery = this.poolNode.parser.astify(structuredClone(queryString));
|
|
297
254
|
} catch (syntaxError) {
|
|
298
|
-
this.warn(
|
|
299
|
-
`Could not parse query for parsedQuery output: ${syntaxError}`
|
|
300
|
-
);
|
|
255
|
+
this.warn(`Could not parse query for parsedQuery output: ${syntaxError}`);
|
|
301
256
|
}
|
|
302
257
|
}
|
|
303
|
-
|
|
304
|
-
if (Object.keys(otherParams).length) {
|
|
305
|
-
newMsg.odbc = otherParams;
|
|
306
|
-
}
|
|
258
|
+
if (Object.keys(otherParams).length) { newMsg.odbc = otherParams; }
|
|
307
259
|
return newMsg;
|
|
308
260
|
};
|
|
309
|
-
|
|
310
|
-
this.
|
|
311
|
-
|
|
312
|
-
let
|
|
313
|
-
let
|
|
314
|
-
let
|
|
315
|
-
|
|
261
|
+
|
|
262
|
+
this.executeStreamQuery = async (dbConnection, queryString, queryParams, msg, send, done) => {
|
|
263
|
+
const chunkSize = parseInt(this.config.streamChunkSize) || 1;
|
|
264
|
+
let cursor;
|
|
265
|
+
let rowCount = 0;
|
|
266
|
+
let chunk = [];
|
|
267
|
+
|
|
316
268
|
try {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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)) {
|
|
335
|
-
if (parsed[0] === "name" || parsed[0] === "&") {
|
|
336
|
-
if (!objPath.has(msg, parsed[1])) {
|
|
337
|
-
this.warn(
|
|
338
|
-
`Mustache parameter "${parsed[1]}" is absent and will render to undefined`
|
|
339
|
-
);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
269
|
+
cursor = await dbConnection.cursor(queryString, queryParams);
|
|
270
|
+
this.status({ fill: "blue", shape: "dot", text: "streaming rows..." });
|
|
271
|
+
let row = await cursor.fetch();
|
|
272
|
+
while (row) {
|
|
273
|
+
rowCount++;
|
|
274
|
+
chunk.push(row);
|
|
275
|
+
if (chunk.length >= chunkSize) {
|
|
276
|
+
const newMsg = RED.util.cloneMessage(msg);
|
|
277
|
+
objPath.set(newMsg, this.config.outputObj, chunk);
|
|
278
|
+
newMsg.odbc_stream = { index: rowCount - chunk.length, count: chunk.length, complete: false };
|
|
279
|
+
send(newMsg);
|
|
280
|
+
chunk = [];
|
|
342
281
|
}
|
|
343
|
-
|
|
344
|
-
currentQueryString,
|
|
345
|
-
msg
|
|
346
|
-
);
|
|
282
|
+
row = await cursor.fetch();
|
|
347
283
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
);
|
|
362
|
-
}
|
|
363
|
-
currentQueryString = msg.query;
|
|
364
|
-
} else if (msg?.payload) {
|
|
365
|
-
// Ensuite msg.payload.query ou msg.payload (si string)
|
|
366
|
-
if (typeof msg.payload === "string") {
|
|
367
|
-
try {
|
|
368
|
-
const payloadJson = JSON.parse(msg.payload);
|
|
369
|
-
if (
|
|
370
|
-
payloadJson?.query &&
|
|
371
|
-
typeof payloadJson.query === "string"
|
|
372
|
-
) {
|
|
373
|
-
currentQueryString = payloadJson.query;
|
|
374
|
-
}
|
|
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;
|
|
383
|
-
}
|
|
284
|
+
if (chunk.length > 0) {
|
|
285
|
+
const newMsg = RED.util.cloneMessage(msg);
|
|
286
|
+
objPath.set(newMsg, this.config.outputObj, chunk);
|
|
287
|
+
newMsg.odbc_stream = { index: rowCount - chunk.length, count: chunk.length, complete: true };
|
|
288
|
+
send(newMsg);
|
|
384
289
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
290
|
+
if (rowCount === 0) {
|
|
291
|
+
const newMsg = RED.util.cloneMessage(msg);
|
|
292
|
+
objPath.set(newMsg, this.config.outputObj, []);
|
|
293
|
+
newMsg.odbc_stream = { index: 0, count: 0, complete: true };
|
|
294
|
+
send(newMsg);
|
|
388
295
|
}
|
|
296
|
+
this.status({ fill: "green", shape: "dot", text: `success (${rowCount} rows)` });
|
|
297
|
+
if(done) done();
|
|
298
|
+
} catch(err) {
|
|
299
|
+
throw err;
|
|
300
|
+
}
|
|
301
|
+
finally {
|
|
302
|
+
if (cursor) await cursor.close();
|
|
303
|
+
}
|
|
304
|
+
};
|
|
389
305
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
306
|
+
this.runQuery = async (msg, send, done) => {
|
|
307
|
+
let currentQueryString = this.config.query || "";
|
|
308
|
+
let currentQueryParams = msg.parameters;
|
|
309
|
+
let isPreparedStatement = false;
|
|
310
|
+
let connectionFromPool = null;
|
|
394
311
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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.
|
|
312
|
+
try {
|
|
313
|
+
this.status({ fill: "blue", shape: "dot", text: "preparing..." });
|
|
314
|
+
this.config.outputObj = msg?.output || this.config?.outputObj || "payload";
|
|
315
|
+
|
|
316
|
+
isPreparedStatement = currentQueryParams || (currentQueryString && currentQueryString.includes("?"));
|
|
317
|
+
if (!isPreparedStatement && currentQueryString) {
|
|
318
|
+
for (const parsed of mustache.parse(currentQueryString)) {
|
|
319
|
+
if ((parsed[0] === "name" || parsed[0] === "&") && !objPath.has(msg, parsed[1])) {
|
|
320
|
+
this.warn(`Mustache parameter "${parsed[1]}" is absent.`);
|
|
428
321
|
}
|
|
429
322
|
}
|
|
430
|
-
|
|
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
|
-
);
|
|
438
|
-
}
|
|
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
|
-
);
|
|
447
|
-
}
|
|
323
|
+
currentQueryString = mustache.render(currentQueryString, msg);
|
|
448
324
|
}
|
|
325
|
+
if (msg?.query) { currentQueryString = msg.query; }
|
|
326
|
+
if (!currentQueryString) { throw new Error("No query to execute"); }
|
|
449
327
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|
-
);
|
|
467
|
-
}
|
|
328
|
+
const execute = async (conn) => {
|
|
329
|
+
if (this.config.streaming) {
|
|
330
|
+
await this.executeStreamQuery(conn, currentQueryString, currentQueryParams, msg, send, done);
|
|
331
|
+
} else {
|
|
332
|
+
const processedMsg = await this.executeQueryAndProcess(conn, currentQueryString, currentQueryParams, isPreparedStatement, msg);
|
|
333
|
+
this.status({ fill: "green", shape: "dot", text: "success" });
|
|
334
|
+
send(processedMsg);
|
|
335
|
+
if(done) done();
|
|
336
|
+
}
|
|
337
|
+
};
|
|
468
338
|
|
|
469
|
-
// --- Première tentative avec une connexion du pool ---
|
|
470
339
|
let firstAttemptError = null;
|
|
471
340
|
try {
|
|
472
341
|
connectionFromPool = await this.poolNode.connect();
|
|
473
|
-
|
|
474
|
-
|
|
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
|
|
342
|
+
await execute(connectionFromPool);
|
|
343
|
+
return;
|
|
500
344
|
} catch (err) {
|
|
501
|
-
firstAttemptError = this.enhanceError(
|
|
502
|
-
|
|
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
|
|
345
|
+
firstAttemptError = this.enhanceError(err, currentQueryString, currentQueryParams, "Query failed with pooled connection");
|
|
346
|
+
this.warn(`First attempt failed: ${firstAttemptError.message}`);
|
|
511
347
|
} 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;
|
|
521
|
-
}
|
|
348
|
+
if (connectionFromPool) await connectionFromPool.close();
|
|
522
349
|
}
|
|
523
350
|
|
|
524
|
-
// --- Si la première tentative a échoué (firstAttemptError est défini) ---
|
|
525
351
|
if (firstAttemptError) {
|
|
526
|
-
if (
|
|
527
|
-
this.poolNode &&
|
|
528
|
-
this.poolNode.config.retryFreshConnection
|
|
529
|
-
) {
|
|
352
|
+
if (this.poolNode && this.poolNode.config.retryFreshConnection) {
|
|
530
353
|
this.log("Attempting retry with a fresh connection.");
|
|
531
|
-
this.status({
|
|
532
|
-
fill: "yellow",
|
|
533
|
-
shape: "dot",
|
|
534
|
-
text: "Retrying (fresh)...",
|
|
535
|
-
});
|
|
536
|
-
|
|
354
|
+
this.status({ fill: "yellow", shape: "dot", text: "Retrying (fresh)..." });
|
|
537
355
|
let freshConnection = null;
|
|
538
356
|
try {
|
|
539
|
-
|
|
540
|
-
|
|
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
|
-
);
|
|
357
|
+
const freshConnectConfig = this.poolNode.getFreshConnectionConfig();
|
|
358
|
+
freshConnection = await odbcModule.connect(freshConnectConfig);
|
|
553
359
|
this.log("Fresh connection established for retry.");
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
});
|
|
572
|
-
send(processedFreshMsg);
|
|
573
|
-
|
|
574
|
-
if (this.poolNode.resetPool) {
|
|
575
|
-
await this.poolNode.resetPool(); // Demander au pool de se réinitialiser
|
|
360
|
+
await execute(freshConnection);
|
|
361
|
+
this.log("Query successful with fresh connection. Resetting pool.");
|
|
362
|
+
await this.poolNode.resetPool();
|
|
363
|
+
return;
|
|
364
|
+
} catch (freshError) {
|
|
365
|
+
this.warn(`Retry with fresh connection also failed: ${freshError.message}`);
|
|
366
|
+
const retryDelay = parseInt(this.poolNode.config.retryDelay) || 0;
|
|
367
|
+
if (retryDelay > 0) {
|
|
368
|
+
this.isAwaitingRetry = true;
|
|
369
|
+
this.status({ fill: "red", shape: "ring", text: `Retry in ${retryDelay}s...` });
|
|
370
|
+
this.log(`Scheduling retry in ${retryDelay} seconds.`);
|
|
371
|
+
this.retryTimer = setTimeout(() => {
|
|
372
|
+
this.isAwaitingRetry = false;
|
|
373
|
+
this.log("Timer expired. Triggering scheduled retry.");
|
|
374
|
+
this.receive(msg);
|
|
375
|
+
}, retryDelay * 1000);
|
|
376
|
+
if (done) done();
|
|
576
377
|
} else {
|
|
577
|
-
this.
|
|
578
|
-
"poolNode.resetPool is not available. Pool cannot be reset automatically."
|
|
579
|
-
);
|
|
378
|
+
throw this.enhanceError(freshError, currentQueryString, currentQueryParams, "Query failed on fresh connection retry");
|
|
580
379
|
}
|
|
581
|
-
|
|
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
380
|
} 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
|
-
}
|
|
605
|
-
}
|
|
381
|
+
if (freshConnection) await freshConnection.close();
|
|
606
382
|
}
|
|
607
383
|
} else {
|
|
608
|
-
// retryFreshConnection n'est pas activé, donc on lance l'erreur de la première tentative
|
|
609
384
|
throw firstAttemptError;
|
|
610
385
|
}
|
|
611
386
|
}
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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
|
-
|
|
627
|
-
if (done) {
|
|
628
|
-
done(finalError); // Passer l'erreur au callback done de Node-RED
|
|
629
|
-
} else {
|
|
630
|
-
this.error(finalError, msg); // Utiliser this.error pour logguer l'erreur correctement
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
}; // Fin de runQuery
|
|
387
|
+
} catch (err) {
|
|
388
|
+
const finalError = err instanceof Error ? err : new Error(String(err));
|
|
389
|
+
this.status({ fill: "red", shape: "ring", text: "query error" });
|
|
390
|
+
if (done) { done(finalError); } else { this.error(finalError, msg); }
|
|
391
|
+
}
|
|
392
|
+
};
|
|
634
393
|
|
|
635
394
|
this.checkPool = async function (msg, send, done) {
|
|
636
395
|
try {
|
|
637
|
-
if (!this.poolNode) {
|
|
638
|
-
throw new Error(
|
|
639
|
-
"ODBC Connection Configuration node is not properly configured or deployed."
|
|
640
|
-
);
|
|
641
|
-
}
|
|
396
|
+
if (!this.poolNode) { throw new Error("ODBC Config node not properly configured."); }
|
|
642
397
|
if (this.poolNode.connecting) {
|
|
643
|
-
// Si le pool est en cours d'initialisation
|
|
644
398
|
this.warn("Waiting for connection pool to initialize...");
|
|
645
|
-
this.status({
|
|
646
|
-
fill: "yellow",
|
|
647
|
-
shape: "ring",
|
|
648
|
-
text: "Waiting for pool",
|
|
649
|
-
});
|
|
399
|
+
this.status({ fill: "yellow", shape: "ring", text: "Waiting for pool" });
|
|
650
400
|
setTimeout(() => {
|
|
651
401
|
this.checkPool(msg, send, done).catch((err) => {
|
|
652
|
-
|
|
653
|
-
this.
|
|
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
|
-
}
|
|
402
|
+
this.status({ fill: "red", shape: "dot", text: "Pool wait failed" });
|
|
403
|
+
if (done) { done(err); } else { this.error(err, msg); }
|
|
663
404
|
});
|
|
664
|
-
}, 1000);
|
|
405
|
+
}, 1000);
|
|
665
406
|
return;
|
|
666
407
|
}
|
|
667
|
-
|
|
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()
|
|
671
|
-
|
|
672
408
|
await this.runQuery(msg, send, done);
|
|
673
409
|
} catch (err) {
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
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
|
-
});
|
|
685
|
-
if (done) {
|
|
686
|
-
done(finalError);
|
|
687
|
-
} else {
|
|
688
|
-
this.error(finalError, msg);
|
|
689
|
-
}
|
|
410
|
+
const finalError = err instanceof Error ? err : new Error(String(err));
|
|
411
|
+
this.status({ fill: "red", shape: "dot", text: "Op failed" });
|
|
412
|
+
if (done) { done(finalError); } else { this.error(finalError, msg); }
|
|
690
413
|
}
|
|
691
414
|
};
|
|
692
415
|
|
|
693
416
|
this.on("input", async (msg, send, done) => {
|
|
694
|
-
|
|
417
|
+
if (this.isAwaitingRetry) {
|
|
418
|
+
if (this.poolNode && this.poolNode.config.retryOnMsg) {
|
|
419
|
+
this.log("New message received, overriding retry timer and attempting query now.");
|
|
420
|
+
clearTimeout(this.retryTimer);
|
|
421
|
+
this.retryTimer = null;
|
|
422
|
+
this.isAwaitingRetry = false;
|
|
423
|
+
} else {
|
|
424
|
+
this.warn("Node is in a retry-wait state. New message ignored as per configuration.");
|
|
425
|
+
if (done) done();
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
695
429
|
try {
|
|
696
430
|
await this.checkPool(msg, send, done);
|
|
697
431
|
} catch (error) {
|
|
698
|
-
const finalError =
|
|
699
|
-
|
|
700
|
-
this.
|
|
701
|
-
fill: "red",
|
|
702
|
-
shape: "ring",
|
|
703
|
-
text: "Input error",
|
|
704
|
-
});
|
|
705
|
-
if (done) {
|
|
706
|
-
done(finalError);
|
|
707
|
-
} else {
|
|
708
|
-
this.error(finalError, msg);
|
|
709
|
-
}
|
|
432
|
+
const finalError = error instanceof Error ? error : new Error(String(error));
|
|
433
|
+
this.status({ fill: "red", shape: "ring", text: "Input error" });
|
|
434
|
+
if (done) { done(finalError); } else { this.error(finalError, msg); }
|
|
710
435
|
}
|
|
711
436
|
});
|
|
712
437
|
|
|
713
438
|
this.on("close", async (done) => {
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
this.status({});
|
|
439
|
+
if (this.retryTimer) {
|
|
440
|
+
clearTimeout(this.retryTimer);
|
|
441
|
+
this.log("Cleared pending retry timer on node close/redeploy.");
|
|
442
|
+
}
|
|
443
|
+
this.status({});
|
|
719
444
|
done();
|
|
720
445
|
});
|
|
721
446
|
|
|
@@ -723,11 +448,9 @@ module.exports = function (RED) {
|
|
|
723
448
|
this.status({ fill: "green", shape: "dot", text: "ready" });
|
|
724
449
|
} else {
|
|
725
450
|
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
|
-
);
|
|
451
|
+
this.warn("ODBC Config node not found or not deployed.");
|
|
729
452
|
}
|
|
730
453
|
}
|
|
731
454
|
|
|
732
455
|
RED.nodes.registerType("odbc", odbc);
|
|
733
|
-
};
|
|
456
|
+
};
|