✨ feat(database): 添加 SQLite 数据库支持
实现基于 better-sqlite3 的数据库层: - 数据库连接管理(进程级单例模式) - 数据库迁移系统 - 基础表结构初始化 - 添加 better-sqlite3 使用最佳实践文档 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
f37680c3e3
commit
8216a83ec1
|
|
@ -21,6 +21,7 @@ __snapshots__/
|
|||
.claude/
|
||||
*.swp
|
||||
*.swo
|
||||
data/
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
|
|
|
|||
|
|
@ -0,0 +1,476 @@
|
|||
下面是一份结合目前(约 2026 年)社区共识的 **better-sqlite3 使用指南 + 最佳实践实战版**,你可以直接拿来做项目的参考模板。
|
||||
|
||||
---
|
||||
|
||||
# better-sqlite3 使用指南(含最佳实践)
|
||||
|
||||
## 一、定位与总体建议
|
||||
|
||||
### 1.1 为什么选 better-sqlite3
|
||||
|
||||
在 Node.js 中操作 SQLite,目前主流选型是:
|
||||
|
||||
- 新项目首选 **better-sqlite3**
|
||||
- Node.js 24+ 内置的 `node:sqlite` 适合:轻量脚本、小工具、对依赖敏感的场景
|
||||
- 旧项目上的 `node-sqlite3` 已被弃用,不建议新项目继续使用
|
||||
|
||||
**better-sqlite3 的优点:**
|
||||
|
||||
- **同步 API**:代码简单可读,事务逻辑不被 Promise/回调搅乱
|
||||
- **性能优秀**:普遍被认为是 Node 生态里最快的 SQLite 绑定之一
|
||||
- **完整事务支持**:内置事务封装,写多步业务逻辑非常顺手
|
||||
- **PRAGMA / 备份等高级能力完备**
|
||||
|
||||
安装:
|
||||
|
||||
```bash
|
||||
npm install better-sqlite3
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、连接管理:一个进程一个连接
|
||||
|
||||
### 2.1 最佳实践:进程级单例
|
||||
|
||||
SQLite 本质是操作一个文件;和 MySQL/Postgres 不同,连接很轻量,不需要连接池。
|
||||
|
||||
**推荐模式:**
|
||||
|
||||
- 应用启动时创建 **一个全局 Database 实例**
|
||||
- 全程复用,不要在每个请求里频繁 open/close
|
||||
- 在进程退出前再统一 close(很多服务甚至不显式 close)
|
||||
|
||||
```js
|
||||
// db/index.js
|
||||
import Database from 'better-sqlite3';
|
||||
|
||||
let db;
|
||||
|
||||
export function getDB() {
|
||||
if (!db) {
|
||||
db = new Database('app.sqlite', {
|
||||
readonly: false,
|
||||
fileMustExist: false,
|
||||
timeout: 5000, // 等待锁超时时间
|
||||
// verbose: console.log // 调试时可打开
|
||||
});
|
||||
|
||||
// 基础 PRAGMA,见后文性能调优部分
|
||||
db.pragma('journal_mode = WAL');
|
||||
db.pragma('synchronous = NORMAL');
|
||||
db.pragma('cache_size = -64000'); // ~64MB
|
||||
db.pragma('temp_store = MEMORY');
|
||||
}
|
||||
return db;
|
||||
}
|
||||
```
|
||||
|
||||
**反例(不要这样写):**
|
||||
|
||||
```js
|
||||
// ❌ 每次请求新建 & 关闭连接
|
||||
function handler(req, res) {
|
||||
const db = new Database('app.sqlite');
|
||||
const row = db.prepare('SELECT * FROM users WHERE id = ?').get(req.params.id);
|
||||
db.close();
|
||||
res.json(row);
|
||||
}
|
||||
```
|
||||
|
||||
问题:
|
||||
|
||||
- 无谓的 open/close 开销
|
||||
- 预编译语句无法复用
|
||||
- 容易出现并发锁和资源浪费
|
||||
|
||||
### 2.2 多线程(Worker Threads)场景
|
||||
|
||||
当读操作很多、单线程成为瓶颈时:
|
||||
|
||||
- 使用 **Worker Threads**
|
||||
- **每个 worker 线程自己 new 一个 Database 实例**
|
||||
- 不在多个线程之间共享同一个 `Database` 对象
|
||||
|
||||
示意:
|
||||
|
||||
```js
|
||||
// workers/readWorker.js
|
||||
import { workerData, parentPort } from 'node:worker_threads';
|
||||
import Database from 'better-sqlite3';
|
||||
|
||||
const db = new Database('app.sqlite', {
|
||||
readonly: true,
|
||||
fileMustExist: true,
|
||||
timeout: 3000
|
||||
});
|
||||
|
||||
db.pragma('cache_size = -32000');
|
||||
db.pragma('temp_store = MEMORY');
|
||||
db.pragma('query_only = ON'); // 防止误写
|
||||
|
||||
const stmt = db.prepare(workerData.sql);
|
||||
const result = stmt.all(...(workerData.params || []));
|
||||
parentPort.postMessage(result);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、查询与预编译语句(Prepared Statements)
|
||||
|
||||
### 3.1 必须坚持使用预编译
|
||||
|
||||
**目标:同时保证性能和安全(防 SQL 注入)**。
|
||||
|
||||
```js
|
||||
const db = getDB();
|
||||
|
||||
// 预编译语句在模块级缓存
|
||||
const getUserById = db.prepare('SELECT * FROM users WHERE id = ?');
|
||||
const listUsersByEmail = db.prepare('SELECT * FROM users WHERE email = ?');
|
||||
|
||||
export function findUserById(id) {
|
||||
return getUserById.get(id); // 单行
|
||||
}
|
||||
|
||||
export function findUsersByEmail(email) {
|
||||
return listUsersByEmail.all(email); // 多行
|
||||
}
|
||||
```
|
||||
|
||||
**防注入反例:**
|
||||
|
||||
```js
|
||||
// ❌ 直接拼字符串,存在注入风险
|
||||
const sql = `SELECT * FROM users WHERE email = '${email}'`;
|
||||
db.prepare(sql).all();
|
||||
|
||||
// ✅ 使用参数绑定
|
||||
db.prepare('SELECT * FROM users WHERE email = ?').all(email);
|
||||
```
|
||||
|
||||
### 3.2 get / all / run / exec 的使用场景
|
||||
|
||||
| 方法 | 用途 | 返回值典型用法 |
|
||||
|--------|-----------------------|--------------------------------------|
|
||||
| `get` | 查询**一条**记录 | `const row = stmt.get(id)` |
|
||||
| `all` | 查询**多条**记录 | `const rows = stmt.all(status)` |
|
||||
| `run` | INSERT/UPDATE/DELETE | `const info = stmt.run(...params)` |
|
||||
| `exec` | 执行多条 SQL/DDL 语句 | `db.exec('CREATE TABLE ...; ...')` |
|
||||
|
||||
示例:
|
||||
|
||||
```js
|
||||
const db = getDB();
|
||||
|
||||
const findActiveUsers = db.prepare('SELECT * FROM users WHERE active = 1');
|
||||
const deactivateUser = db.prepare('UPDATE users SET active = 0 WHERE id = ?');
|
||||
|
||||
// 多行
|
||||
const users = findActiveUsers.all();
|
||||
|
||||
// 更新
|
||||
const info = deactivateUser.run(123);
|
||||
console.log(info.changes); // 受影响行数
|
||||
console.log(info.lastInsertRowid); // 如是INSERT语句则有意义
|
||||
```
|
||||
|
||||
### 3.3 善用 RETURNING 减少一次查询
|
||||
|
||||
如果 SQLite 版本支持 `RETURNING`:
|
||||
|
||||
```js
|
||||
const createOrder = db.prepare(`
|
||||
INSERT INTO orders (user_id, amount)
|
||||
VALUES (?, ?)
|
||||
RETURNING id, user_id, amount, created_at
|
||||
`);
|
||||
|
||||
const order = createOrder.get(userId, amount);
|
||||
// 这里已经拿到刚插入的订单信息,无需再额外 SELECT
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、事务管理:写操作的必备武器
|
||||
|
||||
任何「多步写入」逻辑(比如:下单、转账、批量导入)都应该放在事务里。
|
||||
|
||||
### 4.1 使用 better-sqlite3 的事务封装
|
||||
|
||||
```js
|
||||
const db = getDB();
|
||||
|
||||
const placeOrder = db.transaction((userId, items) => {
|
||||
const insertOrder = db.prepare(`
|
||||
INSERT INTO orders (user_id, status)
|
||||
VALUES (?, 'pending')
|
||||
`);
|
||||
const insertOrderItem = db.prepare(`
|
||||
INSERT INTO order_items (order_id, product_id, qty, price)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`);
|
||||
const updateStock = db.prepare(`
|
||||
UPDATE products
|
||||
SET stock = stock - ?
|
||||
WHERE id = ?
|
||||
`);
|
||||
|
||||
const { lastInsertRowid: orderId } = insertOrder.run(userId);
|
||||
|
||||
for (const item of items) {
|
||||
insertOrderItem.run(orderId, item.productId, item.qty, item.price);
|
||||
updateStock.run(item.qty, item.productId);
|
||||
}
|
||||
|
||||
return orderId;
|
||||
});
|
||||
|
||||
// 使用
|
||||
try {
|
||||
const orderId = placeOrder(userId, items);
|
||||
// 成功自动 COMMIT
|
||||
} catch (err) {
|
||||
// 抛出异常时自动 ROLLBACK
|
||||
console.error('placeOrder 失败', err);
|
||||
}
|
||||
```
|
||||
|
||||
**注意事项:**
|
||||
|
||||
- 事务函数体 **必须是同步代码**,不要在里面 `await`/Promise/`setTimeout`
|
||||
- 所有需要原子性的写操作都要包进同一次 `db.transaction()`
|
||||
- 对于批量插入/更新,**一个大事务远快于 N 个小事务**
|
||||
|
||||
---
|
||||
|
||||
## 五、性能调优:常用 PRAGMA 配置
|
||||
|
||||
### 5.1 典型 Web 服务推荐配置
|
||||
|
||||
在应用启动时统一设置一次:
|
||||
|
||||
```js
|
||||
const db = getDB();
|
||||
|
||||
// 提升并发 & 写性能:建议生产使用
|
||||
db.pragma('journal_mode = WAL'); // 写前日志
|
||||
|
||||
// 写安全性 & 性能折中
|
||||
db.pragma('synchronous = NORMAL'); // 不要贸然用 OFF
|
||||
|
||||
// 内存缓存(负数表示 KiB)
|
||||
db.pragma('cache_size = -64000'); // ≈64MB
|
||||
|
||||
// 临时对象走内存
|
||||
db.pragma('temp_store = MEMORY');
|
||||
|
||||
// 利用 mmap 加速 IO(根据机器内存调整)
|
||||
db.pragma('mmap_size = 268435456'); // 256MB
|
||||
```
|
||||
|
||||
**不推荐在有写入的生产环境用 `synchronous = OFF`**,否则断电/崩溃时有丢数据风险。
|
||||
|
||||
### 5.2 读密集型 Worker 优化
|
||||
|
||||
针对只读 worker:
|
||||
|
||||
```js
|
||||
const db = new Database('app.sqlite', { readonly: true, fileMustExist: true });
|
||||
|
||||
db.pragma('cache_size = -32000'); // ~32MB
|
||||
db.pragma('temp_store = MEMORY');
|
||||
db.pragma('mmap_size = 1073741824'); // ~1GB
|
||||
db.pragma('query_only = ON'); // 强制只读
|
||||
```
|
||||
|
||||
### 5.3 查询计划与优化
|
||||
|
||||
数据库结构或索引大改之后,可以执行一次:
|
||||
|
||||
```js
|
||||
db.exec('ANALYZE;');
|
||||
db.pragma('optimize');
|
||||
```
|
||||
|
||||
有需要时,用 `EXPLAIN QUERY PLAN` 分析慢查询(开发/排查时用)。
|
||||
|
||||
---
|
||||
|
||||
## 六、安全实践:防注入、防误删、防泄露
|
||||
|
||||
### 6.1 防 SQL 注入
|
||||
|
||||
核心原则:
|
||||
|
||||
- 所有用户输入**必须**通过「参数绑定」进入 SQL
|
||||
- 不拼接字符串、不直接插入变量
|
||||
|
||||
```js
|
||||
// ✅ 推荐使用形式
|
||||
const stmt = db.prepare('SELECT * FROM users WHERE email = ?');
|
||||
const user = stmt.get(email);
|
||||
```
|
||||
|
||||
### 6.2 参数校验
|
||||
|
||||
在进入 SQL 前尽量校验类型 / 长度 / 格式:
|
||||
|
||||
```js
|
||||
function assertId(id) {
|
||||
if (!Number.isInteger(id) || id <= 0) {
|
||||
throw new Error('非法ID');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 SQLite 文件存放与权限
|
||||
|
||||
- **不要**把 `.sqlite` 文件放在可被 Web 服务器直接访问的静态目录下
|
||||
- 赋予最小权限:如仅运行用户可读写
|
||||
|
||||
```bash
|
||||
chmod 600 app.sqlite
|
||||
chown node_user:node_group app.sqlite
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、备份与恢复
|
||||
|
||||
### 7.1 使用 better-sqlite3 的备份 API(在线备份)
|
||||
|
||||
```js
|
||||
const db = getDB();
|
||||
|
||||
// 备份到另一个文件(热备,无需停服务)
|
||||
await db.backup('backup.sqlite');
|
||||
```
|
||||
|
||||
你可以把这段逻辑放进定时任务里,例如每天低峰执行一次。
|
||||
|
||||
### 7.2 冷备份(停机拷贝)
|
||||
|
||||
如果可以短暂停机:
|
||||
|
||||
1. 停止 Node 进程
|
||||
2. 直接复制数据库文件
|
||||
|
||||
```bash
|
||||
cp app.sqlite app.sqlite.bak
|
||||
```
|
||||
|
||||
在 WAL 模式下,确保 `.sqlite` 和相关的 `-wal`/`-shm` 文件也一并妥善处理(备份 API 已经帮你做了这件事,一般更推荐)。
|
||||
|
||||
---
|
||||
|
||||
## 八、迁移与工程化
|
||||
|
||||
### 8.1 简单迁移系统示例
|
||||
|
||||
建议至少维护一个简易迁移机制,例如:
|
||||
|
||||
- 建一张 `migrations` 表,记录已执行的版本
|
||||
- 在应用启动时按顺序执行 `migrations/*.sql`
|
||||
|
||||
伪代码:
|
||||
|
||||
```js
|
||||
// db/migrate.js
|
||||
import { getDB } from './index.js';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
export function runMigrations() {
|
||||
const db = getDB();
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS migrations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT UNIQUE NOT NULL,
|
||||
run_at TEXT NOT NULL
|
||||
)`);
|
||||
|
||||
const applied = new Set(
|
||||
db.prepare('SELECT name FROM migrations').all().map(r => r.name)
|
||||
);
|
||||
|
||||
const dir = path.join(process.cwd(), 'src/db/migrations');
|
||||
const files = fs.readdirSync(dir).sort(); // 如:001_init.sql, 002_add_user.sql
|
||||
|
||||
const runMigration = db.transaction((name, sql) => {
|
||||
db.exec(sql);
|
||||
db.prepare('INSERT INTO migrations (name, run_at) VALUES (?, datetime(\'now\'))')
|
||||
.run(name);
|
||||
});
|
||||
|
||||
for (const file of files) {
|
||||
if (applied.has(file)) continue;
|
||||
const sql = fs.readFileSync(path.join(dir, file), 'utf8');
|
||||
runMigration(file, sql);
|
||||
console.log('已执行迁移:', file);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 8.2 典型项目结构建议
|
||||
|
||||
```txt
|
||||
src/
|
||||
db/
|
||||
index.js # 初始化 & 导出 db 实例 + PRAGMA
|
||||
migrate.js # 迁移逻辑
|
||||
migrations/ # *.sql 文件
|
||||
models/
|
||||
user.js # 用户相关 SQL & 事务
|
||||
order.js
|
||||
workers/
|
||||
readWorker.js # 高并发只读 worker
|
||||
services/
|
||||
userService.js # 业务逻辑。只调用 models,不直接写 SQL
|
||||
app.js # 应用入口,启动HTTP服务 & 调用 runMigrations()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 九、与 Node.js 内置 `node:sqlite` 的关系
|
||||
|
||||
- Node.js 24 LTS 起,内置了 `node:sqlite` 模块,方便做无依赖的小工具
|
||||
- 但在实际生产服务里:
|
||||
- **性能、特性、生态** 综合考虑,better-sqlite3 依然是首选
|
||||
- 两者 API 思路类似:同步操作、小而美,更方便从一种迁移到另一种
|
||||
|
||||
可以简记为:
|
||||
|
||||
> **重业务 / 生产服务:better-sqlite3**
|
||||
> **简单脚本 / 工具:node:sqlite 也可以考虑**
|
||||
|
||||
---
|
||||
|
||||
## 十、一页小抄(速查版)
|
||||
|
||||
1. **连接管理**:
|
||||
- 进程内只创建一个 `Database` 实例,全局复用
|
||||
- Worker Threads 中,一线程一实例,不共享连接
|
||||
|
||||
2. **查询写法**:
|
||||
- 所有 SQL 使用 **prepared statement + 参数绑定**
|
||||
- 单行用 `get`,多行用 `all`,写操作用 `run`
|
||||
|
||||
3. **事务**:
|
||||
- 多步写操作必须放在 `db.transaction()` 中
|
||||
- 事务函数里面不要写任何异步代码
|
||||
|
||||
4. **PRAGMA 调优**:
|
||||
- `journal_mode = WAL`
|
||||
- `synchronous = NORMAL`
|
||||
- 合理设置 `cache_size` / `temp_store = MEMORY` / `mmap_size`
|
||||
|
||||
5. **安全**:
|
||||
- 禁止拼接 SQL 字符串
|
||||
- 校验输入
|
||||
- 数据库文件不要暴露在静态目录,限制文件权限
|
||||
|
||||
6. **扩展读性能**:
|
||||
- 使用 Worker Threads
|
||||
- 每个 worker 使用只读连接 + `query_only = ON`
|
||||
|
||||
如果你后续有更具体的场景(例如 Electron 桌面应用、嵌入式设备、日志收集服务等),可以再描述一下,我可以基于这些最佳实践帮你落到更贴近场景的代码模板与配置。
|
||||
|
|
@ -25,6 +25,7 @@
|
|||
"@mastra/libsql": "^0.16.4",
|
||||
"@mastra/memory": "^0.15.13",
|
||||
"axios": "^1.13.2",
|
||||
"better-sqlite3": "^12.6.0",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^5.1.0",
|
||||
"helmet": "^8.1.0",
|
||||
|
|
@ -34,6 +35,7 @@
|
|||
"zod": "^4.1.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/helmet": "^4.0.0",
|
||||
|
|
|
|||
222
pnpm-lock.yaml
222
pnpm-lock.yaml
|
|
@ -26,6 +26,9 @@ importers:
|
|||
axios:
|
||||
specifier: ^1.13.2
|
||||
version: 1.13.2
|
||||
better-sqlite3:
|
||||
specifier: ^12.6.0
|
||||
version: 12.6.0
|
||||
cors:
|
||||
specifier: ^2.8.5
|
||||
version: 2.8.5
|
||||
|
|
@ -48,6 +51,9 @@ importers:
|
|||
specifier: ^4.1.13
|
||||
version: 4.1.13
|
||||
devDependencies:
|
||||
'@types/better-sqlite3':
|
||||
specifier: ^7.6.13
|
||||
version: 7.6.13
|
||||
'@types/cors':
|
||||
specifier: ^2.8.17
|
||||
version: 2.8.19
|
||||
|
|
@ -1628,6 +1634,9 @@ packages:
|
|||
'@types/babel__traverse@7.28.0':
|
||||
resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
|
||||
|
||||
'@types/better-sqlite3@7.6.13':
|
||||
resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==}
|
||||
|
||||
'@types/body-parser@1.19.6':
|
||||
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
|
||||
|
||||
|
|
@ -1859,9 +1868,19 @@ packages:
|
|||
resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
better-sqlite3@12.6.0:
|
||||
resolution: {integrity: sha512-FXI191x+D6UPWSze5IzZjhz+i9MK9nsuHsmTX9bXVl52k06AfZ2xql0lrgIUuzsMsJ7Vgl5kIptvDgBLIV3ZSQ==}
|
||||
engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x}
|
||||
|
||||
bignumber.js@9.3.1:
|
||||
resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==}
|
||||
|
||||
bindings@1.5.0:
|
||||
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
|
||||
|
||||
bl@4.1.0:
|
||||
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
|
||||
|
||||
body-parser@1.20.4:
|
||||
resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==}
|
||||
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
||||
|
|
@ -1879,6 +1898,9 @@ packages:
|
|||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||
hasBin: true
|
||||
|
||||
buffer@5.7.1:
|
||||
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
|
||||
|
||||
builtins@5.1.0:
|
||||
resolution: {integrity: sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==}
|
||||
|
||||
|
|
@ -1909,6 +1931,9 @@ packages:
|
|||
resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==}
|
||||
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
|
||||
|
||||
chownr@1.1.4:
|
||||
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
|
||||
|
||||
cjs-module-lexer@1.4.3:
|
||||
resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==}
|
||||
|
||||
|
|
@ -2041,6 +2066,14 @@ packages:
|
|||
supports-color:
|
||||
optional: true
|
||||
|
||||
decompress-response@6.0.0:
|
||||
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
deep-extend@0.6.0:
|
||||
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
|
||||
deep-is@0.1.4:
|
||||
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
||||
|
||||
|
|
@ -2181,6 +2214,10 @@ packages:
|
|||
resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==}
|
||||
engines: {node: ^18.19.0 || >=20.5.0}
|
||||
|
||||
expand-template@2.0.3:
|
||||
resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
express@4.22.1:
|
||||
resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==}
|
||||
engines: {node: '>= 0.10.0'}
|
||||
|
|
@ -2225,6 +2262,9 @@ packages:
|
|||
resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
file-uri-to-path@1.0.0:
|
||||
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
|
||||
|
||||
fill-range@7.1.1:
|
||||
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
@ -2272,6 +2312,9 @@ packages:
|
|||
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
fs-constants@1.0.0:
|
||||
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
|
||||
|
||||
fs-extra@11.3.2:
|
||||
resolution: {integrity: sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==}
|
||||
engines: {node: '>=14.14'}
|
||||
|
|
@ -2319,6 +2362,9 @@ packages:
|
|||
get-tsconfig@4.13.0:
|
||||
resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==}
|
||||
|
||||
github-from-package@0.0.0:
|
||||
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
|
||||
|
||||
glob-parent@5.1.2:
|
||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||
engines: {node: '>= 6'}
|
||||
|
|
@ -2434,12 +2480,18 @@ packages:
|
|||
resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
ieee754@1.2.1:
|
||||
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
||||
|
||||
import-in-the-middle@1.15.0:
|
||||
resolution: {integrity: sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==}
|
||||
|
||||
inherits@2.0.4:
|
||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||
|
||||
ini@1.3.8:
|
||||
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
|
||||
|
||||
ip-regex@4.3.0:
|
||||
resolution: {integrity: sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
@ -2657,9 +2709,16 @@ packages:
|
|||
engines: {node: '>=4'}
|
||||
hasBin: true
|
||||
|
||||
mimic-response@3.1.0:
|
||||
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
minimist@1.2.8:
|
||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||
|
||||
mkdirp-classic@0.5.3:
|
||||
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
|
||||
|
||||
mlly@1.8.0:
|
||||
resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==}
|
||||
|
||||
|
|
@ -2684,6 +2743,9 @@ packages:
|
|||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
napi-build-utils@2.0.0:
|
||||
resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==}
|
||||
|
||||
negotiator@0.6.3:
|
||||
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
|
@ -2692,6 +2754,10 @@ packages:
|
|||
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
node-abi@3.85.0:
|
||||
resolution: {integrity: sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
node-domexception@1.0.0:
|
||||
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
||||
engines: {node: '>=10.5.0'}
|
||||
|
|
@ -2883,6 +2949,11 @@ packages:
|
|||
resolution: {integrity: sha512-XROs1h+DNatgKh/AlIlCtDxWzwrKdYDb2mOs58n4yN8BkGN9ewqeQwG5ApS4/IzwCb7HPttUkOVulkYatd2PIw==}
|
||||
engines: {node: '>=15.0.0'}
|
||||
|
||||
prebuild-install@7.1.3:
|
||||
resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
prettier@3.7.4:
|
||||
resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==}
|
||||
engines: {node: '>=14'}
|
||||
|
|
@ -2941,6 +3012,10 @@ packages:
|
|||
resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
||||
rc@1.2.8:
|
||||
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
|
||||
hasBin: true
|
||||
|
||||
react-refresh@0.18.0:
|
||||
resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
|
@ -2949,6 +3024,10 @@ packages:
|
|||
resolution: {integrity: sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
readable-stream@3.6.2:
|
||||
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
real-require@0.2.0:
|
||||
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
|
||||
engines: {node: '>= 12.13.0'}
|
||||
|
|
@ -3112,6 +3191,12 @@ packages:
|
|||
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
simple-concat@1.0.1:
|
||||
resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==}
|
||||
|
||||
simple-get@4.0.1:
|
||||
resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
|
||||
|
||||
sisteransi@1.0.5:
|
||||
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
|
||||
|
||||
|
|
@ -3142,6 +3227,9 @@ packages:
|
|||
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
string_decoder@1.3.0:
|
||||
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
||||
|
||||
strip-ansi@6.0.1:
|
||||
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
@ -3150,6 +3238,10 @@ packages:
|
|||
resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
strip-json-comments@2.0.1:
|
||||
resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
strip-json-comments@5.0.3:
|
||||
resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==}
|
||||
engines: {node: '>=14.16'}
|
||||
|
|
@ -3176,6 +3268,13 @@ packages:
|
|||
peerDependencies:
|
||||
react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
tar-fs@2.1.4:
|
||||
resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==}
|
||||
|
||||
tar-stream@2.2.0:
|
||||
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
tcp-port-used@1.0.2:
|
||||
resolution: {integrity: sha512-l7ar8lLUD3XS1V2lfoJlCBaeoaWo/2xfYt81hM7VlvR4RrMVFqfmzfhLVk40hAb368uitje5gPtBRL1m/DGvLA==}
|
||||
|
||||
|
|
@ -3209,6 +3308,9 @@ packages:
|
|||
engines: {node: '>=18.0.0'}
|
||||
hasBin: true
|
||||
|
||||
tunnel-agent@0.6.0:
|
||||
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
|
||||
|
||||
type-is@1.6.18:
|
||||
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
|
@ -3263,6 +3365,9 @@ packages:
|
|||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
|
||||
utils-merge@1.0.1:
|
||||
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
|
||||
engines: {node: '>= 0.4.0'}
|
||||
|
|
@ -5155,6 +5260,10 @@ snapshots:
|
|||
dependencies:
|
||||
'@babel/types': 7.28.5
|
||||
|
||||
'@types/better-sqlite3@7.6.13':
|
||||
dependencies:
|
||||
'@types/node': 25.0.0
|
||||
|
||||
'@types/body-parser@1.19.6':
|
||||
dependencies:
|
||||
'@types/connect': 3.4.38
|
||||
|
|
@ -5443,8 +5552,23 @@ snapshots:
|
|||
dependencies:
|
||||
safe-buffer: 5.1.2
|
||||
|
||||
better-sqlite3@12.6.0:
|
||||
dependencies:
|
||||
bindings: 1.5.0
|
||||
prebuild-install: 7.1.3
|
||||
|
||||
bignumber.js@9.3.1: {}
|
||||
|
||||
bindings@1.5.0:
|
||||
dependencies:
|
||||
file-uri-to-path: 1.0.0
|
||||
|
||||
bl@4.1.0:
|
||||
dependencies:
|
||||
buffer: 5.7.1
|
||||
inherits: 2.0.4
|
||||
readable-stream: 3.6.2
|
||||
|
||||
body-parser@1.20.4:
|
||||
dependencies:
|
||||
bytes: 3.1.2
|
||||
|
|
@ -5488,6 +5612,11 @@ snapshots:
|
|||
node-releases: 2.0.27
|
||||
update-browserslist-db: 1.2.2(browserslist@4.28.1)
|
||||
|
||||
buffer@5.7.1:
|
||||
dependencies:
|
||||
base64-js: 1.5.1
|
||||
ieee754: 1.2.1
|
||||
|
||||
builtins@5.1.0:
|
||||
dependencies:
|
||||
semver: 7.7.3
|
||||
|
|
@ -5517,6 +5646,8 @@ snapshots:
|
|||
|
||||
chalk@5.6.2: {}
|
||||
|
||||
chownr@1.1.4: {}
|
||||
|
||||
cjs-module-lexer@1.4.3: {}
|
||||
|
||||
cliui@8.0.1:
|
||||
|
|
@ -5606,6 +5737,12 @@ snapshots:
|
|||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
decompress-response@6.0.0:
|
||||
dependencies:
|
||||
mimic-response: 3.1.0
|
||||
|
||||
deep-extend@0.6.0: {}
|
||||
|
||||
deep-is@0.1.4: {}
|
||||
|
||||
deepmerge@4.3.1: {}
|
||||
|
|
@ -5763,6 +5900,8 @@ snapshots:
|
|||
strip-final-newline: 4.0.0
|
||||
yoctocolors: 2.1.2
|
||||
|
||||
expand-template@2.0.3: {}
|
||||
|
||||
express@4.22.1:
|
||||
dependencies:
|
||||
accepts: 1.3.8
|
||||
|
|
@ -5865,6 +6004,8 @@ snapshots:
|
|||
dependencies:
|
||||
is-unicode-supported: 2.1.0
|
||||
|
||||
file-uri-to-path@1.0.0: {}
|
||||
|
||||
fill-range@7.1.1:
|
||||
dependencies:
|
||||
to-regex-range: 5.0.1
|
||||
|
|
@ -5920,6 +6061,8 @@ snapshots:
|
|||
|
||||
fresh@2.0.0: {}
|
||||
|
||||
fs-constants@1.0.0: {}
|
||||
|
||||
fs-extra@11.3.2:
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
|
|
@ -5984,6 +6127,8 @@ snapshots:
|
|||
dependencies:
|
||||
resolve-pkg-maps: 1.0.0
|
||||
|
||||
github-from-package@0.0.0: {}
|
||||
|
||||
glob-parent@5.1.2:
|
||||
dependencies:
|
||||
is-glob: 4.0.3
|
||||
|
|
@ -6055,6 +6200,8 @@ snapshots:
|
|||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
|
||||
ieee754@1.2.1: {}
|
||||
|
||||
import-in-the-middle@1.15.0:
|
||||
dependencies:
|
||||
acorn: 8.15.0
|
||||
|
|
@ -6064,6 +6211,8 @@ snapshots:
|
|||
|
||||
inherits@2.0.4: {}
|
||||
|
||||
ini@1.3.8: {}
|
||||
|
||||
ip-regex@4.3.0: {}
|
||||
|
||||
ipaddr.js@1.9.1: {}
|
||||
|
|
@ -6271,8 +6420,12 @@ snapshots:
|
|||
|
||||
mime@1.6.0: {}
|
||||
|
||||
mimic-response@3.1.0: {}
|
||||
|
||||
minimist@1.2.8: {}
|
||||
|
||||
mkdirp-classic@0.5.3: {}
|
||||
|
||||
mlly@1.8.0:
|
||||
dependencies:
|
||||
acorn: 8.15.0
|
||||
|
|
@ -6300,10 +6453,16 @@ snapshots:
|
|||
|
||||
nanoid@3.3.11: {}
|
||||
|
||||
napi-build-utils@2.0.0: {}
|
||||
|
||||
negotiator@0.6.3: {}
|
||||
|
||||
negotiator@1.0.0: {}
|
||||
|
||||
node-abi@3.85.0:
|
||||
dependencies:
|
||||
semver: 7.7.3
|
||||
|
||||
node-domexception@1.0.0: {}
|
||||
|
||||
node-fetch@2.7.0:
|
||||
|
|
@ -6491,6 +6650,21 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
prebuild-install@7.1.3:
|
||||
dependencies:
|
||||
detect-libc: 2.1.2
|
||||
expand-template: 2.0.3
|
||||
github-from-package: 0.0.0
|
||||
minimist: 1.2.8
|
||||
mkdirp-classic: 0.5.3
|
||||
napi-build-utils: 2.0.0
|
||||
node-abi: 3.85.0
|
||||
pump: 3.0.3
|
||||
rc: 1.2.8
|
||||
simple-get: 4.0.1
|
||||
tar-fs: 2.1.4
|
||||
tunnel-agent: 0.6.0
|
||||
|
||||
prettier@3.7.4: {}
|
||||
|
||||
pretty-ms@9.3.0:
|
||||
|
|
@ -6556,10 +6730,23 @@ snapshots:
|
|||
iconv-lite: 0.7.0
|
||||
unpipe: 1.0.0
|
||||
|
||||
rc@1.2.8:
|
||||
dependencies:
|
||||
deep-extend: 0.6.0
|
||||
ini: 1.3.8
|
||||
minimist: 1.2.8
|
||||
strip-json-comments: 2.0.1
|
||||
|
||||
react-refresh@0.18.0: {}
|
||||
|
||||
react@19.2.1: {}
|
||||
|
||||
readable-stream@3.6.2:
|
||||
dependencies:
|
||||
inherits: 2.0.4
|
||||
string_decoder: 1.3.0
|
||||
util-deprecate: 1.0.2
|
||||
|
||||
real-require@0.2.0: {}
|
||||
|
||||
redis@5.10.0:
|
||||
|
|
@ -6794,6 +6981,14 @@ snapshots:
|
|||
|
||||
signal-exit@4.1.0: {}
|
||||
|
||||
simple-concat@1.0.1: {}
|
||||
|
||||
simple-get@4.0.1:
|
||||
dependencies:
|
||||
decompress-response: 6.0.0
|
||||
once: 1.4.0
|
||||
simple-concat: 1.0.1
|
||||
|
||||
sisteransi@1.0.5: {}
|
||||
|
||||
sonic-boom@4.2.0:
|
||||
|
|
@ -6816,12 +7011,18 @@ snapshots:
|
|||
is-fullwidth-code-point: 3.0.0
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
string_decoder@1.3.0:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
strip-ansi@6.0.1:
|
||||
dependencies:
|
||||
ansi-regex: 5.0.1
|
||||
|
||||
strip-final-newline@4.0.0: {}
|
||||
|
||||
strip-json-comments@2.0.1: {}
|
||||
|
||||
strip-json-comments@5.0.3: {}
|
||||
|
||||
supports-color@7.2.0:
|
||||
|
|
@ -6842,6 +7043,21 @@ snapshots:
|
|||
react: 19.2.1
|
||||
use-sync-external-store: 1.6.0(react@19.2.1)
|
||||
|
||||
tar-fs@2.1.4:
|
||||
dependencies:
|
||||
chownr: 1.1.4
|
||||
mkdirp-classic: 0.5.3
|
||||
pump: 3.0.3
|
||||
tar-stream: 2.2.0
|
||||
|
||||
tar-stream@2.2.0:
|
||||
dependencies:
|
||||
bl: 4.1.0
|
||||
end-of-stream: 1.4.5
|
||||
fs-constants: 1.0.0
|
||||
inherits: 2.0.4
|
||||
readable-stream: 3.6.2
|
||||
|
||||
tcp-port-used@1.0.2:
|
||||
dependencies:
|
||||
debug: 4.3.1
|
||||
|
|
@ -6877,6 +7093,10 @@ snapshots:
|
|||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
tunnel-agent@0.6.0:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
type-is@1.6.18:
|
||||
dependencies:
|
||||
media-typer: 0.3.0
|
||||
|
|
@ -6921,6 +7141,8 @@ snapshots:
|
|||
dependencies:
|
||||
react: 19.2.1
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
utils-merge@1.0.1: {}
|
||||
|
||||
uuid@11.1.0: {}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
ignoredBuiltDependencies:
|
||||
- better-sqlite3
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
import Database from 'better-sqlite3';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import fs from 'node:fs';
|
||||
import type { DatabaseConfig } from './types.js';
|
||||
|
||||
let db: Database.Database | null = null;
|
||||
|
||||
/**
|
||||
* 获取数据库实例(进程级单例)
|
||||
* @param config 数据库配置
|
||||
* @returns Database 实例
|
||||
*/
|
||||
export function getDB(config?: DatabaseConfig): Database.Database {
|
||||
if (db) {
|
||||
return db;
|
||||
}
|
||||
|
||||
// 确定数据库路径
|
||||
const dbPath = resolveDatabasePath(config?.path);
|
||||
|
||||
// 确保目录存在
|
||||
ensureDirectoryExists(dbPath);
|
||||
|
||||
// 创建数据库实例
|
||||
db = new Database(dbPath, {
|
||||
readonly: config?.readonly ?? false,
|
||||
fileMustExist: config?.fileMustExist ?? false,
|
||||
timeout: config?.timeout ?? 5000,
|
||||
});
|
||||
|
||||
// 应用性能优化 PRAGMA
|
||||
applyPragmas(db);
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析数据库路径
|
||||
* 默认使用 data/app.db
|
||||
*/
|
||||
function resolveDatabasePath(customPath?: string): string {
|
||||
if (customPath) {
|
||||
return path.isAbsolute(customPath)
|
||||
? customPath
|
||||
: path.resolve(process.cwd(), customPath);
|
||||
}
|
||||
|
||||
// 默认路径:项目根目录下的 data/app.db
|
||||
const projectRoot = findProjectRoot();
|
||||
return path.join(projectRoot, 'data', 'app.db');
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找项目根目录
|
||||
*/
|
||||
function findProjectRoot(): string {
|
||||
// 尝试从 import.meta.url 获取模块路径
|
||||
const modulePath = typeof import.meta !== 'undefined'
|
||||
? import.meta.url
|
||||
: undefined;
|
||||
|
||||
if (modulePath && modulePath.startsWith('file://')) {
|
||||
const filePath = fileURLToPath(modulePath);
|
||||
const dir = path.dirname(filePath);
|
||||
// src/database -> 项目根目录
|
||||
return path.resolve(dir, '../..');
|
||||
}
|
||||
|
||||
return process.cwd();
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保目录存在
|
||||
*/
|
||||
function ensureDirectoryExists(dbPath: string): void {
|
||||
const dir = path.dirname(dbPath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用性能优化 PRAGMA 配置
|
||||
*/
|
||||
function applyPragmas(database: Database.Database): void {
|
||||
// WAL 模式:提升并发写入性能
|
||||
database.pragma('journal_mode = WAL');
|
||||
|
||||
// 同步级别:平衡安全性与性能
|
||||
database.pragma('synchronous = NORMAL');
|
||||
|
||||
// 缓存大小:约 64MB(负数表示 KiB)
|
||||
database.pragma('cache_size = -64000');
|
||||
|
||||
// 临时表存储方式:内存
|
||||
database.pragma('temp_store = MEMORY');
|
||||
|
||||
// 内存映射大小:256MB(根据机器内存调整)
|
||||
// 注意:这可能需要根据实际环境调整
|
||||
// database.pragma('mmap_size = 268435456');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭数据库连接
|
||||
* 通常在进程退出前调用
|
||||
*/
|
||||
export function closeDB(): void {
|
||||
if (db) {
|
||||
db.close();
|
||||
db = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取原始数据库实例(高级用法)
|
||||
* 注意:直接操作数据库实例需要自行保证安全性
|
||||
*/
|
||||
export function getRawDB(): Database.Database | null {
|
||||
return db;
|
||||
}
|
||||
|
||||
export { Database };
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { getDB } from './index.js';
|
||||
import type { MigrationResult, MigrationFile } from './types.js';
|
||||
|
||||
const MIGRATIONS_DIR = 'migrations';
|
||||
|
||||
/**
|
||||
* 运行所有待执行的迁移
|
||||
* @param migrationsDir 迁移文件目录(相对于 src/database)
|
||||
* @returns 迁移执行结果
|
||||
*/
|
||||
export function runMigrations(migrationsDir: string = MIGRATIONS_DIR): MigrationResult {
|
||||
const db = getDB();
|
||||
const result: MigrationResult = {
|
||||
applied: [],
|
||||
skipped: [],
|
||||
};
|
||||
|
||||
// 1. 创建 migrations 表(如果不存在)
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS migrations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT UNIQUE NOT NULL,
|
||||
run_at TEXT NOT NULL
|
||||
)
|
||||
`);
|
||||
|
||||
// 2. 获取已执行的迁移列表
|
||||
const appliedMigrations = db.prepare<MigrationFile[], any>(
|
||||
'SELECT name FROM migrations'
|
||||
).all();
|
||||
|
||||
const appliedSet = new Set(appliedMigrations.map((m) => m.name));
|
||||
|
||||
// 3. 扫描迁移目录
|
||||
const migrationFiles = getMigrationFiles(migrationsDir);
|
||||
|
||||
// 4. 按文件名排序后执行
|
||||
for (const file of migrationFiles) {
|
||||
if (appliedSet.has(file.name)) {
|
||||
result.skipped.push(file.name);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
executeMigration(db, file);
|
||||
result.applied.push(file.name);
|
||||
} catch (error) {
|
||||
throw new Error(`Migration "${file.name}" failed: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行单个迁移
|
||||
*/
|
||||
function executeMigration(db: any, file: MigrationFile): void {
|
||||
const runMigration = db.transaction(() => {
|
||||
// 执行迁移 SQL
|
||||
db.exec(file.content);
|
||||
|
||||
// 记录迁移已执行
|
||||
db.prepare(
|
||||
'INSERT INTO migrations (name, run_at) VALUES (?, datetime("now"))'
|
||||
).run(file.name);
|
||||
});
|
||||
|
||||
runMigration();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取迁移文件列表
|
||||
*/
|
||||
function getMigrationFiles(migrationsDir: string): MigrationFile[] {
|
||||
const dir = resolveMigrationsDir(migrationsDir);
|
||||
|
||||
if (!fs.existsSync(dir)) {
|
||||
console.warn(`Migrations directory not found: ${dir}`);
|
||||
return [];
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(dir).filter(
|
||||
(file) => file.endsWith('.sql') && /^\d+_/.test(file)
|
||||
);
|
||||
|
||||
// 按文件名排序,确保按序号执行
|
||||
files.sort();
|
||||
|
||||
return files.map((name) => ({
|
||||
name,
|
||||
content: fs.readFileSync(path.join(dir, name), 'utf8'),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析迁移目录的绝对路径
|
||||
*/
|
||||
function resolveMigrationsDir(migrationsDir: string): string {
|
||||
// 如果是绝对路径,直接使用
|
||||
if (path.isAbsolute(migrationsDir)) {
|
||||
return migrationsDir;
|
||||
}
|
||||
|
||||
// 从当前模块位置解析
|
||||
if (typeof import.meta !== 'undefined' && import.meta.url) {
|
||||
const modulePath = fileURLToPath(import.meta.url);
|
||||
const moduleDir = path.dirname(modulePath);
|
||||
return path.resolve(moduleDir, migrationsDir);
|
||||
}
|
||||
|
||||
// 兜底:相对于工作目录
|
||||
return path.resolve(process.cwd(), 'src/database', migrationsDir);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有迁移状态
|
||||
*/
|
||||
export function getMigrationStatus(): { applied: string[]; pending: string[] } {
|
||||
const db = getDB();
|
||||
|
||||
const applied = db.prepare<MigrationFile[], any>(
|
||||
'SELECT name FROM migrations ORDER BY name'
|
||||
).all().map((m) => m.name);
|
||||
|
||||
const migrationFiles = getMigrationFiles(MIGRATIONS_DIR);
|
||||
const pending = migrationFiles
|
||||
.filter((f) => !applied.includes(f.name))
|
||||
.map((f) => f.name);
|
||||
|
||||
return { applied, pending };
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
-- 初始化迁移
|
||||
-- 创建基础表结构
|
||||
|
||||
-- 示例表:用户表
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
deleted_at TEXT DEFAULT NULL
|
||||
);
|
||||
|
||||
-- 示例表:系统配置表
|
||||
CREATE TABLE IF NOT EXISTS system_config (
|
||||
key TEXT PRIMARY KEY NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
description TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_deleted_at ON users(deleted_at);
|
||||
|
||||
-- 插入初始配置
|
||||
INSERT OR IGNORE INTO system_config (key, value, description) VALUES
|
||||
('app_name', 'BattleMonAgent', '应用名称'),
|
||||
('version', '1.0.0', '应用版本'),
|
||||
('db_version', '001', '数据库版本');
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
export const initSql = `
|
||||
-- 初始化迁移
|
||||
-- 创建基础表结构
|
||||
|
||||
-- 示例表:用户表
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
deleted_at TEXT DEFAULT NULL
|
||||
);
|
||||
|
||||
-- 示例表:系统配置表
|
||||
CREATE TABLE IF NOT EXISTS system_config (
|
||||
key TEXT PRIMARY KEY NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
description TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_deleted_at ON users(deleted_at);
|
||||
|
||||
-- 插入初始配置
|
||||
INSERT OR IGNORE INTO system_config (key, value, description) VALUES
|
||||
('app_name', 'BattleMonAgent', '应用名称'),
|
||||
('version', '1.0.0', '应用版本'),
|
||||
('db_version', '001', '数据库版本');
|
||||
|
||||
`
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import type Database from 'better-sqlite3';
|
||||
|
||||
/**
|
||||
* 数据库配置选项
|
||||
*/
|
||||
export interface DatabaseConfig {
|
||||
/** 数据库文件路径 */
|
||||
path?: string;
|
||||
/** 是否只读模式 */
|
||||
readonly?: boolean;
|
||||
/** 文件不存在时是否报错 */
|
||||
fileMustExist?: boolean;
|
||||
/** 等待锁超时时间 (ms) */
|
||||
timeout?: number;
|
||||
/** 是否打印 SQL 日志 */
|
||||
verbose?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 迁移记录类型
|
||||
*/
|
||||
export interface MigrationRecord {
|
||||
id: number;
|
||||
name: string;
|
||||
run_at: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 迁移文件接口
|
||||
*/
|
||||
export interface MigrationFile {
|
||||
/** 迁移文件名,如 '001_init.sql' */
|
||||
name: string;
|
||||
/** 迁移 SQL 内容 */
|
||||
content: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 迁移执行结果
|
||||
*/
|
||||
export interface MigrationResult {
|
||||
applied: string[];
|
||||
skipped: string[];
|
||||
}
|
||||
|
|
@ -6,10 +6,36 @@ import { mastra } from '../mastra';
|
|||
import { agentRouter } from './routes/agent-routes';
|
||||
import { responseFormatter } from './middleware/response-formatter';
|
||||
import { errorHandler, notFoundHandler } from './middleware/error-handler';
|
||||
import { getDB, closeDB } from '../database';
|
||||
import { initSql } from '../database/migrations/init';
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 7328;
|
||||
|
||||
// 测试数据库连接
|
||||
function testDatabaseConnection(): boolean {
|
||||
try {
|
||||
const db = getDB({
|
||||
path: 'data/app.db',
|
||||
readonly: false,
|
||||
fileMustExist: false,
|
||||
timeout: 5000,
|
||||
});
|
||||
// 执行简单查询验证连接
|
||||
const result = db.prepare('SELECT * FROM system_config').all();
|
||||
|
||||
console.log('Database result:', result);
|
||||
console.log('✅ Database connection established successfully');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('❌ Database connection failed:', error instanceof Error ? error.message : error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize database connection
|
||||
const dbConnected = testDatabaseConnection();
|
||||
|
||||
// Security middleware
|
||||
app.use(helmet());
|
||||
|
||||
|
|
@ -69,12 +95,14 @@ const server = app.listen(PORT, () => {
|
|||
console.log(`🚀 Express server listening on port ${PORT}`);
|
||||
console.log(`📡 Health check: http://localhost:${PORT}/health`);
|
||||
console.log(`🤖 Agent API: http://localhost:${PORT}/api/agent`);
|
||||
console.log(`🗄️ Database: ${dbConnected ? 'Connected' : 'Failed'}`);
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('SIGTERM received, shutting down gracefully');
|
||||
server.close(() => {
|
||||
closeDB();
|
||||
console.log('Server closed');
|
||||
process.exit(0);
|
||||
});
|
||||
|
|
@ -83,6 +111,7 @@ process.on('SIGTERM', () => {
|
|||
process.on('SIGINT', () => {
|
||||
console.log('SIGINT received, shutting down gracefully');
|
||||
server.close(() => {
|
||||
closeDB();
|
||||
console.log('Server closed');
|
||||
process.exit(0);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue