tauri-plugin-libsql-api
v0.1.0
Published
Tauri plugin for libsql with encryption support
Readme
Tauri libsql 插件
一个用于 libsql 的 Tauri 插件,内置 AES-256-CBC 加密、Drizzle ORM 支持,以及浏览器兼容的迁移运行器。
为什么选择这个插件?
1. Rust ORM 在应用开发中很痛苦
在 Rust 中使用原始 SQL 很冗长,而 Rust ORM(Diesel、SeaORM)需要在 Rust 中定义模式,与 TypeScript 前端配合不佳,并且增加了显著的构建复杂性。对于真正的业务逻辑在 TypeScript 中的 Tauri 应用,你也希望在 TypeScript 中编写数据库代码。
2. 无需 Node.js 运行时的 Drizzle ORM
Drizzle ORM 非常出色 —— 类型安全的查询、简洁的迁移系统、出色的开发体验。但它通常需要 Node.js 或 Bun 运行时来直接打开数据库文件。Tauri 的 WebView 没有这样的运行时。
这个插件通过 Drizzle 的 sqlite-proxy 模式解决了这个问题:Drizzle 生成 SQL,代理通过 Tauri 的 invoke() 将其发送到 Rust 插件,Rust 插件使用 libsql 执行它。你的 TypeScript 代码使用完整的 Drizzle ORM,零 Node.js 依赖。
3. 在 WebView 中工作的迁移
Drizzle 内置的迁移器使用 Node 的 fs 模块在运行时从磁盘读取 .sql 文件 —— 这在浏览器/WebView 环境中不存在。有两种解决方法:
- Tauri 资源文件夹 —— 将文件打包为应用资源,通过 Tauri 的 asset 协议读取。可以工作,但需要额外的 Tauri 配置。
- Vite
import.meta.glob(这个插件的方法) —— Vite 在构建时将 SQL 文件内容直接打包到 JavaScript 中。无需运行时文件系统访问,无需额外配置。
// Vite 在构建时解析这些 —— SQL 文本被内联到 JS 包中
const migrations = import.meta.glob<string>("./drizzle/*.sql", {
eager: true,
query: "?raw",
import: "default",
});
await migrate("sqlite:myapp.db", migrations);这个插件中的 migrate() 函数接收预加载的 SQL 字符串,在 __drizzle_migrations 表中跟踪已应用的迁移,并按顺序运行待处理的迁移。
4. 内置加密
@tauri-apps/plugin-sql(使用 sqlx)不支持加密。这个插件使用 libsql 的原生 AES-256-CBC 加密,无需额外的原生库或 FFI 包装器。
功能特性
- 完整的 SQLite 兼容性 通过 libsql
- 原生加密 —— AES-256-CBC,可在插件级别或每个数据库配置
- Drizzle ORM 集成 —— sqlite-proxy 模式与
createDrizzleProxy - 迁移运行器 —— 浏览器安全的
migrate(),通过 Vite 在构建时打包 SQL 文件 - API 兼容 适用于
@tauri-apps/plugin-sql(在适用的地方) - 跨平台:macOS、Windows、Linux、iOS、Android
已测试
- [x] MacOS
- [ ] Windows
- [ ] Linux
- [ ] iOS
- [ ] Android
安装
Rust
[dependencies]
tauri-plugin-libsql = "0.1.0"JavaScript / TypeScript
npm install tauri-plugin-libsql-api
# 或
pnpm add tauri-plugin-libsql-api快速开始
1. 注册插件(Rust)
// src-tauri/src/lib.rs
// 默认:数据库相对于当前工作目录解析
tauri::Builder::default()
.plugin(tauri_plugin_libsql::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");要在固定位置存储数据库:
use std::path::PathBuf;
let config = tauri_plugin_libsql::Config {
base_path: Some(PathBuf::from("/path/to/data")),
encryption: None,
};
tauri::Builder::default()
.plugin(tauri_plugin_libsql::init_with_config(config))
.run(tauri::generate_context!())
.expect("error while running tauri application");2. 使用 Database 类(TypeScript)
import { Database } from 'tauri-plugin-libsql-api';
const db = await Database.load('sqlite:myapp.db');
await db.execute(
'INSERT INTO users (name) VALUES ($1)',
['Alice']
);
const users = await db.select<{ id: number; name: string }[]>(
'SELECT * FROM users'
);
await db.close();数据库位置
相对路径(例如 sqlite:myapp.db)相对于 base_path 解析:
- 默认:
std::env::current_dir()—— 你启动 Tauri 进程的目录 - 自定义:在插件配置中设置
base_path(见上文) - 绝对路径 按原样使用
- 内存中:
sqlite::memory:
相对路径被规范化(.. 组件被折叠)并且必须保持在 base_path 内。会逸出的路径(例如 sqlite:../../secret)将被拒绝并返回错误。
Drizzle ORM 集成
设置
import { drizzle } from 'drizzle-orm/sqlite-proxy';
import { createDrizzleProxy } from 'tauri-plugin-libsql-api';
import * as schema from './schema';
const db = drizzle(createDrizzleProxy('sqlite:myapp.db'), { schema });
const users = await db.select().from(schema.users);createDrizzleProxy 在首次使用时延迟加载数据库连接,因此使用它时无需单独调用 Database.load()。
使用加密
import { createDrizzleProxyWithEncryption } from 'tauri-plugin-libsql-api';
const db = drizzle(
createDrizzleProxyWithEncryption({
path: 'sqlite:encrypted.db',
encryption: {
cipher: 'aes256cbc',
key: myKey32Bytes, // number[] | Uint8Array, 32 字节
},
}),
{ schema }
);迁移
标准的 drizzle-orm/sqlite-proxy/migrator 在运行时从文件系统读取,这在 Tauri WebView 中无法工作。这个插件提供了一个 migrate() 函数,它接受由 Vite 的 import.meta.glob 预打包的 SQL 内容。
工作流程
1. 定义你的模式 (src/lib/schema.ts):
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
export const users = sqliteTable('users', {
id: integer('id').primaryKey({ autoIncrement: true }),
name: text('name').notNull(),
});2. 配置 drizzle-kit (drizzle.config.ts):
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
dialect: 'sqlite',
schema: './src/lib/schema.ts',
out: './drizzle',
});3. 生成迁移文件:
npx drizzle-kit generate
# 创建 drizzle/0000_init.sql, drizzle/0001_add_column.sql, 等等4. 在启动时运行迁移:
import { Database, migrate } from 'tauri-plugin-libsql-api';
// Vite 在构建时将这些 SQL 文件打包到应用中
const migrations = import.meta.glob<string>('./drizzle/*.sql', {
eager: true,
query: '?raw',
import: 'default',
});
// 启动顺序:加载 → 迁移 → 查询
await Database.load('sqlite:myapp.db');
await migrate('sqlite:myapp.db', migrations);
// 现在可以安全地查询
const db = drizzle(createDrizzleProxy('sqlite:myapp.db'), { schema });migrate() 如何工作
- 如果不存在,创建
__drizzle_migrations跟踪表 - 通过数字前缀解析迁移文件名(
0000_、0001_等) - 仅按顺序应用待处理的迁移
- 通过文件名记录每个已应用的迁移
添加模式更改
# 1. 编辑 src/lib/schema.ts
# 2. 生成新迁移
npx drizzle-kit generate
# 3. 新迁移在下次应用启动时自动运行选项
await migrate('sqlite:myapp.db', migrations, {
migrationsTable: '__my_migrations', // 默认:'__drizzle_migrations'
});加密
插件级别加密(应用于所有数据库)
在 Rust 中配置一次 —— 前端从不处理密钥:
let config = tauri_plugin_libsql::Config {
base_path: None,
encryption: Some(tauri_plugin_libsql::EncryptionConfig {
cipher: tauri_plugin_libsql::Cipher::Aes256Cbc,
key: my_32_byte_key, // Vec<u8>, 正好 32 字节
}),
};每个数据库加密(从前端)
const key = new Uint8Array(32);
crypto.getRandomValues(key);
const db = await Database.load({
path: 'sqlite:secrets.db',
encryption: {
cipher: 'aes256cbc',
key: Array.from(key), // number[] 或 Uint8Array
},
});安全注意事项:
- AES-256-CBC 需要正好 32 字节
- 将密钥存储在操作系统钥匙串或安全存储中 —— 丢失密钥 = 丢失数据
- 首选插件级别加密;它将密钥排除在 JavaScript 之外
API 参考
Database.load(pathOrOptions)
// 简单用法
const db = await Database.load('sqlite:myapp.db');
// 使用加密
const db = await Database.load({
path: 'sqlite:myapp.db',
encryption: { cipher: 'aes256cbc', key: myKey },
});db.execute(query, values?)
const result = await db.execute(
'INSERT INTO todos (title) VALUES ($1)',
['Buy milk']
);
// result.rowsAffected, result.lastInsertIddb.select<T>(query, values?)
const rows = await db.select<{ id: number; title: string }[]>(
'SELECT * FROM todos WHERE completed = $1',
[0]
);db.batch(queries)
在单个事务中原子执行多个 SQL 语句。用于 DDL 或批量 DML。语句不能使用绑定参数($1 占位符)—— 对参数化查询使用 execute()。
await db.batch([
'CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)',
'CREATE INDEX idx_users_name ON users(name)',
]);db.sync()
从 Turso 远程拉取最新更改到本地副本。对纯本地数据库无操作(无错误返回)。需要 replication 功能。
await db.sync();db.close()
await db.close();migrate(dbPath, migrationFiles, options?)
import { migrate } from 'tauri-plugin-libsql-api';
const migrations = import.meta.glob<string>('./drizzle/*.sql', {
eager: true,
query: '?raw',
import: 'default',
});
await migrate('sqlite:myapp.db', migrations);createDrizzleProxy(dbPath)
返回一个用于 drizzle() 的 sqlite-proxy 回调。延迟加载连接。
createDrizzleProxyWithEncryption(options)
同上,但带加密配置。
getConfig()
import { getConfig } from 'tauri-plugin-libsql-api';
const { encrypted } = await getConfig();权限
添加到你的 tauri.conf.json:
{
"plugins": {
"libsql": {}
}
}或配置细粒度能力:
{
"identifier": "libsql:default",
"permissions": [
"libsql:allow-load",
"libsql:allow-batch",
"libsql:allow-execute",
"libsql:allow-select",
"libsql:allow-close"
]
}与 @tauri-apps/plugin-sql 的比较
| 功能 | tauri-plugin-libsql | @tauri-apps/plugin-sql | |---------|---------------------|------------------------| | SQLite | ✅ libsql | ✅ sqlx | | 加密 | ✅ 内置 AES-256-CBC | ❌ | | Drizzle ORM | ✅ | ✅ | | 迁移运行器 | ✅ 浏览器安全 | ❌ | | MySQL / PostgreSQL | ❌ | ✅ | | API 兼容性 | 部分 | 完整 |
Turso / 远程数据库
该插件支持两种由 libsql 提供支持的远程连接模式。
嵌入式副本(推荐用于 Tauri)
本地 SQLite 文件与 Turso 云数据库保持同步。查询从本地文件读取(快速、离线可用),写入同步到远程。
1. 在你的应用 Cargo.toml 中启用 replication 功能:
tauri-plugin-libsql = { version = "0.1.0", features = ["replication"] }2. 使用 syncUrl 和 authToken 加载:
import { Database, migrate } from 'tauri-plugin-libsql-api';
const db = await Database.load({
path: 'sqlite:local.db', // 本地副本文件
syncUrl: 'libsql://mydb-org.turso.io',
authToken: 'your-turso-auth-token',
});
// 按需同步(例如在应用恢复 / 网络重连时)
await db.sync();在 Database.load() 时,初始同步将最新数据从 Turso 拉取到本地文件。后续的 sync() 调用拉取增量更改。
使用 Drizzle ORM:
const migrations = import.meta.glob<string>('./drizzle/*.sql', {
eager: true, query: '?raw', import: 'default',
});
const db = await Database.load({
path: 'sqlite:local.db',
syncUrl: 'libsql://mydb-org.turso.io',
authToken: import.meta.env.VITE_TURSO_AUTH_TOKEN,
});
await migrate(db.path, migrations);
const drizzleDb = drizzle(createDrizzleProxy(db.path), { schema });纯远程
所有查询直接在 Turso 上执行 —— 没有本地文件。每个查询都需要网络。
启用 remote 功能:
tauri-plugin-libsql = { version = "0.1.0", features = ["remote"] }const db = await Database.load({
path: 'libsql://mydb-org.turso.io',
authToken: 'your-turso-auth-token',
});对于大多数 Tauri 应用,嵌入式副本是更好的选择 —— 它离线工作,读取速度明显更快。
关于
batch()与嵌入式副本的注意事项:在某些版本中,libsql 的execute_batch()不能正确地通过嵌入式副本层路由写入。该插件使用显式BEGIN/COMMIT事务内的单个execute()调用来避免这个问题。
关于 URL 验证的注意事项:libsql 的构建器在内部对同步 URL 调用
unwrap(),格式错误的值(例如前导/尾随空格、错误的协议)可能导致 panic。该插件将其包装在catch_unwind中,因此错误的 URL 会作为适当的错误显示,而不是无限期挂起 IPC。
包大小
基于包含的 Todo List 演示应用(macOS、aarch64、release 构建):
| 格式 | 带加密 | 不带加密 |
|--------|----------------|--------------------|
| .app 包 | 15 MB | 15 MB |
| .dmg 安装程序 | 6.0 MB | 5.9 MB |
禁用加密基本上节省不了什么 —— 与始终存在的 SQLite 原生库相比,AES 密码代码可以忽略不计。encryption 功能标志仍然存在,以避免编译加密相关代码,如果你想在编译时强制没有数据库可以被加密。
禁用加密
加密是默认功能。要选择退出,请禁用默认功能并仅选择你需要的:
Cargo.toml(在你的 Tauri 应用中):
tauri-plugin-libsql = { version = "0.1.0", default-features = false, features = ["core"] }可用功能:
| 功能 | 默认 | 描述 |
|---------|---------|-------------|
| core | ✅ | 本地 SQLite 数据库(始终需要)|
| encryption | ✅ | 通过 libsql 的 AES-256-CBC 加密 |
| replication | ❌ | libsql 复制支持(添加 TLS)|
| remote | ❌ | 远程数据库支持(计划中,见下文)|
当 encryption 被禁用时,向 Database.load() 传递 EncryptionConfig 将在运行时返回错误。TypeScript API 表面保持不变 —— 无需重新构建你的 JS 代码。
使用 AI 集成此插件
仓库根目录包含一个 SKILL.md 文件。它包含有关插件架构、启动顺序、迁移工作流、加密模式和常见错误的结构化上下文 —— 专为 AI 编码助手(Claude Code、Cursor、Copilot 等)编写。
使用 Claude Code
将 SKILL.md 复制到你项目的 .claude/skills/tauri-plugin-libsql/ 目录:
mkdir -p .claude/skills/tauri-plugin-libsql
cp /path/to/tauri-plugin-libsql/SKILL.md .claude/skills/tauri-plugin-libsql/Claude Code 自动发现技能。复制后,你可以自然地提示:
"使用 tauri-plugin-libsql 为我的 Tauri 应用添加一个
notes表。包括模式、迁移和启动顺序。"
Claude 将应用正确的启动顺序,对迁移使用 import.meta.glob,并处理 drizzle 代理模式,无需额外指导。
使用其他 AI 工具
直接将 SKILL.md 的内容粘贴到你的系统提示或上下文窗口中,然后描述你想构建什么。该技能涵盖足够的上下文,让 AI 能在第一次尝试就生成正确、可工作的代码。
项目结构
tauri-plugin-libsql/
├── src/ # Rust 插件
│ ├── lib.rs # 插件初始化、命令注册
│ ├── commands.rs # load、execute、select、close、ping
│ ├── wrapper.rs # DbConnection 包装 libsql
│ ├── decode.rs # libsql::Value → serde_json::Value
│ ├── models.rs # Cipher、EncryptionConfig、QueryResult
│ ├── error.rs # 错误类型
│ ├── desktop.rs # 桌面配置 & base_path
│ └── mobile.rs # 移动端存根
├── guest-js/ # TypeScript 源代码
│ ├── index.ts # Database 类、getConfig、重新导出
│ ├── drizzle.ts # createDrizzleProxy、createDrizzleProxyWithEncryption
│ └── migrate.ts # migrate() —— 浏览器安全的迁移运行器
├── permissions/ # Tauri 权限文件
├── examples/todo-list/ # 演示:带 Drizzle + 迁移的 Todo 应用(15 MB .app / 6 MB .dmg)
├── SKILL.md # 适用于 Claude Code 和其他助手的 AI 技能上下文
├── build.rs
├── Cargo.toml
└── package.json