tangbao-he-db-helper 1.0.21 → 1.0.24
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 +20 -3
- package/db-config.js +139 -3
- package/db-crud.html +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,6 +29,7 @@ MyBatis-style database helper for Node-RED with MySQL and SQLite support.
|
|
|
29
29
|
|
|
30
30
|
- Node.js >= 18.0
|
|
31
31
|
- Node-RED >= 4.0.0
|
|
32
|
+
- **For SQLite support**: `better-sqlite3` >= 12.0.0 (installed separately)
|
|
32
33
|
|
|
33
34
|
## Installation
|
|
34
35
|
|
|
@@ -41,7 +42,7 @@ Or install from a local `.tgz` file:
|
|
|
41
42
|
|
|
42
43
|
```bash
|
|
43
44
|
cd ~/.node-red
|
|
44
|
-
npm install /path/to/tangbao-he-db-helper
|
|
45
|
+
npm install /path/to/tangbao-he-db-helper-*.tgz
|
|
45
46
|
```
|
|
46
47
|
|
|
47
48
|
**For SQLite support, also install:**
|
|
@@ -72,7 +73,8 @@ When you select a driver in the configuration panel, the available fields automa
|
|
|
72
73
|
- **SQLite**: Database File Path only (e.g., `/data/mydb.db` or `./mydb.db`)
|
|
73
74
|
|
|
74
75
|
**MySQL Settings:**
|
|
75
|
-
- Host, Port, Database
|
|
76
|
+
- Host, Port, Database
|
|
77
|
+
- User / Password (stored in Node-RED credentials, encrypted at rest)
|
|
76
78
|
- Charset (default: `UTF8_GENERAL_CI`)
|
|
77
79
|
- Timezone (default: `local`)
|
|
78
80
|
- Connection Pool Limit (default: `50`)
|
|
@@ -163,6 +165,8 @@ Pre-built CRUD operations. No SQL writing needed.
|
|
|
163
165
|
**SQLite Upsert Note:**
|
|
164
166
|
SQLite uses `ON CONFLICT(id) DO UPDATE SET col = excluded.col` instead of MySQL's `ON DUPLICATE KEY UPDATE col = VALUES(col)`. The CRUD node automatically selects the correct syntax based on your configured driver.
|
|
165
167
|
|
|
168
|
+
> **Important:** For `upsertBatch` to work with SQLite, the target table must have a **PRIMARY KEY** or **UNIQUE** constraint on the `idColumn`. Otherwise SQLite will raise a conflict resolution error.
|
|
169
|
+
|
|
166
170
|
**Array to IN:**
|
|
167
171
|
|
|
168
172
|
For `selectList`, `selectOne`, `selectCount`, and `selectPage`, array values are automatically converted to `IN` conditions:
|
|
@@ -250,10 +254,23 @@ msg.params = { userName: "Alice", userAge: 20 };
|
|
|
250
254
|
|
|
251
255
|
2. Restart Node-RED.
|
|
252
256
|
|
|
253
|
-
3. Create a `tangbao-db-config` node, select **SQLite** as the driver, and enter the database file path (e.g., `./data.db`).
|
|
257
|
+
3. Create a `tangbao-db-config` node, select **SQLite** as the driver, and enter the database file path (e.g., `./data.db` or an absolute path like `/data/mydb.db`).
|
|
254
258
|
|
|
255
259
|
4. Use `tangbao-db-crud` or `tangbao-db-query` / `tangbao-db-execute` as usual. All CRUD operations work the same way.
|
|
256
260
|
|
|
261
|
+
### SQLite vs MySQL Quick Reference
|
|
262
|
+
|
|
263
|
+
| Feature | MySQL | SQLite |
|
|
264
|
+
|---------|-------|--------|
|
|
265
|
+
| Connection | TCP (host:port) | Local file |
|
|
266
|
+
| Auth | User / Password | None |
|
|
267
|
+
| Pooling | Yes (configurable) | No (single connection) |
|
|
268
|
+
| Auto-reconnect | Yes | N/A |
|
|
269
|
+
| `upsertBatch` | `ON DUPLICATE KEY UPDATE` | `ON CONFLICT DO UPDATE` (requires PK/UNIQUE) |
|
|
270
|
+
| `deleteAndInsertBatch` | Transaction | Transaction |
|
|
271
|
+
| Table discovery | `information_schema` | `sqlite_master` |
|
|
272
|
+
| Multi-statement execute | Supported with params | Supported **without** params |
|
|
273
|
+
|
|
257
274
|
## Logging Configuration (日志配置)
|
|
258
275
|
|
|
259
276
|
本节点包使用 `node.log` / `node.error` 输出运行日志,其输出级别受 Node-RED 全局日志配置控制。
|
package/db-config.js
CHANGED
|
@@ -180,6 +180,117 @@ module.exports = function(RED) {
|
|
|
180
180
|
}
|
|
181
181
|
};
|
|
182
182
|
|
|
183
|
+
/**
|
|
184
|
+
* 安全拆分 SQL 语句(排除字符串和注释内的分号)
|
|
185
|
+
*/
|
|
186
|
+
function splitSqlStatements(sql) {
|
|
187
|
+
var statements = [];
|
|
188
|
+
var current = '';
|
|
189
|
+
var inString = false;
|
|
190
|
+
var stringChar = null;
|
|
191
|
+
var inComment = false;
|
|
192
|
+
var commentType = null; // 'line' or 'block'
|
|
193
|
+
var i = 0;
|
|
194
|
+
while (i < sql.length) {
|
|
195
|
+
var ch = sql[i];
|
|
196
|
+
var next = sql[i + 1];
|
|
197
|
+
if (inComment) {
|
|
198
|
+
if (commentType === 'line' && ch === '\n') {
|
|
199
|
+
inComment = false;
|
|
200
|
+
commentType = null;
|
|
201
|
+
} else if (commentType === 'block' && ch === '*' && next === '/') {
|
|
202
|
+
inComment = false;
|
|
203
|
+
commentType = null;
|
|
204
|
+
i++;
|
|
205
|
+
}
|
|
206
|
+
current += ch;
|
|
207
|
+
} else if (inString) {
|
|
208
|
+
if (ch === '\\' && i + 1 < sql.length) {
|
|
209
|
+
current += ch + sql[i + 1];
|
|
210
|
+
i++;
|
|
211
|
+
} else if (ch === stringChar) {
|
|
212
|
+
inString = false;
|
|
213
|
+
stringChar = null;
|
|
214
|
+
current += ch;
|
|
215
|
+
} else {
|
|
216
|
+
current += ch;
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
219
|
+
if (ch === '-' && next === '-') {
|
|
220
|
+
inComment = true;
|
|
221
|
+
commentType = 'line';
|
|
222
|
+
current += ch;
|
|
223
|
+
} else if (ch === '/' && next === '*') {
|
|
224
|
+
inComment = true;
|
|
225
|
+
commentType = 'block';
|
|
226
|
+
current += ch;
|
|
227
|
+
} else if (ch === "'" || ch === '"') {
|
|
228
|
+
inString = true;
|
|
229
|
+
stringChar = ch;
|
|
230
|
+
current += ch;
|
|
231
|
+
} else if (ch === ';') {
|
|
232
|
+
statements.push(current.trim());
|
|
233
|
+
current = '';
|
|
234
|
+
} else {
|
|
235
|
+
current += ch;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
i++;
|
|
239
|
+
}
|
|
240
|
+
if (current.trim()) {
|
|
241
|
+
statements.push(current.trim());
|
|
242
|
+
}
|
|
243
|
+
return statements;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* 统计 SQL 中 ? 占位符数量(排除字符串/注释内)
|
|
248
|
+
*/
|
|
249
|
+
function countPlaceholders(sql) {
|
|
250
|
+
var count = 0;
|
|
251
|
+
var inString = false;
|
|
252
|
+
var stringChar = null;
|
|
253
|
+
var inComment = false;
|
|
254
|
+
var commentType = null;
|
|
255
|
+
var i = 0;
|
|
256
|
+
while (i < sql.length) {
|
|
257
|
+
var ch = sql[i];
|
|
258
|
+
var next = sql[i + 1];
|
|
259
|
+
if (inComment) {
|
|
260
|
+
if (commentType === 'line' && ch === '\n') {
|
|
261
|
+
inComment = false;
|
|
262
|
+
commentType = null;
|
|
263
|
+
} else if (commentType === 'block' && ch === '*' && next === '/') {
|
|
264
|
+
inComment = false;
|
|
265
|
+
commentType = null;
|
|
266
|
+
i++;
|
|
267
|
+
}
|
|
268
|
+
} else if (inString) {
|
|
269
|
+
if (ch === '\\' && i + 1 < sql.length) {
|
|
270
|
+
i++;
|
|
271
|
+
} else if (ch === stringChar) {
|
|
272
|
+
inString = false;
|
|
273
|
+
stringChar = null;
|
|
274
|
+
}
|
|
275
|
+
} else {
|
|
276
|
+
if (ch === '-' && next === '-') {
|
|
277
|
+
inComment = true;
|
|
278
|
+
commentType = 'line';
|
|
279
|
+
} else if (ch === '/' && next === '*') {
|
|
280
|
+
inComment = true;
|
|
281
|
+
commentType = 'block';
|
|
282
|
+
} else if (ch === "'" || ch === '"') {
|
|
283
|
+
inString = true;
|
|
284
|
+
stringChar = ch;
|
|
285
|
+
} else if (ch === '?') {
|
|
286
|
+
count++;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
i++;
|
|
290
|
+
}
|
|
291
|
+
return count;
|
|
292
|
+
}
|
|
293
|
+
|
|
183
294
|
/**
|
|
184
295
|
* SQLite 驱动实现
|
|
185
296
|
*/
|
|
@@ -227,9 +338,34 @@ module.exports = function(RED) {
|
|
|
227
338
|
if (!this.db) {
|
|
228
339
|
throw new Error('SQLite database is not available');
|
|
229
340
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
341
|
+
params = params || [];
|
|
342
|
+
// 安全拆分多语句:排除字符串/注释内的分号
|
|
343
|
+
var statements = splitSqlStatements(sql);
|
|
344
|
+
if (statements.length === 0) {
|
|
345
|
+
throw new Error('No valid SQL statement found');
|
|
346
|
+
}
|
|
347
|
+
var totalAffected = 0;
|
|
348
|
+
var lastInsertId = null;
|
|
349
|
+
var paramIdx = 0;
|
|
350
|
+
for (var i = 0; i < statements.length; i++) {
|
|
351
|
+
var stmtSql = statements[i].trim();
|
|
352
|
+
if (!stmtSql) continue;
|
|
353
|
+
// 统计该语句中的 ? 占位符数量(排除字符串内的)
|
|
354
|
+
var placeholderCount = countPlaceholders(stmtSql);
|
|
355
|
+
var stmtParams = params.slice(paramIdx, paramIdx + placeholderCount);
|
|
356
|
+
paramIdx += placeholderCount;
|
|
357
|
+
if (placeholderCount === 0) {
|
|
358
|
+
this.db.exec(stmtSql);
|
|
359
|
+
} else {
|
|
360
|
+
var stmt = this.db.prepare(stmtSql);
|
|
361
|
+
var info = stmt.run(stmtParams);
|
|
362
|
+
totalAffected += info.changes || 0;
|
|
363
|
+
if (info.lastInsertRowid != null) {
|
|
364
|
+
lastInsertId = info.lastInsertRowid;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return { affectedRows: totalAffected, insertId: lastInsertId };
|
|
233
369
|
} catch (err) {
|
|
234
370
|
this.node.error('SQLite execute failed: ' + err.message);
|
|
235
371
|
throw err;
|
package/db-crud.html
CHANGED
|
@@ -159,7 +159,7 @@
|
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
$select.html('<option value="">-- 加载中 --</option>');
|
|
162
|
-
$.getJSON('
|
|
162
|
+
$.getJSON('tangbao-db-config/' + dbConfigId + '/tables', function(tables) {
|
|
163
163
|
var html = '<option value="">-- 请选择表 --</option>';
|
|
164
164
|
var hasCurrent = false;
|
|
165
165
|
tables.forEach(function(t) {
|