@bunny.net/数据库客户端
一个小型 SQL 客户端 兔子数据库。它没有依赖性和用途 fetch 所以同样的代码在Bunny Edge脚本(Deno),Bun,和Node上运行.
[尖叫声] 仅限服务器侧. 永远不要将它运送给浏览器或其他任何不信任的客户端.
一个认证符允许访问整个数据库,这个客户端发送原始的SQL. 输入客户端代码 每位访客都可以读写每张桌子 不管你的UI提供什么. 为什么没有浏览器 有细节.
安装
bun add @bunny.net/database-client
快开始
connect() 读取 BUNNY_DATABASE_URL 财务报告和审定财务报表 BUNNY_DATABASE_AUTH_TOKEN a. 与《公约》有关的其他事项 bunny db quickstart --lang typescript 您的指纹( 或 bunny db create --token --save-env 直接写入),且边缘脚本已经设置的 :
import { connect } from "@bunny.net/database-client";
const db = connect();
const user = await db.prepare("SELECT * FROM users WHERE id = ?").bind(1).first();
需要时明确通过:
const db = connect({
url: "libsql://your-db.lite.bunnydb.net",
authToken: "your-token",
});
Edge 脚本、 Bun、 节点、 Hono、 Next.js、 Astro 和 SvelteKit 的可运行示例 packages/database-client/examples。 。 。 .
API 密码
connect(config?)
回返a Database。每个选项都是可选的.
| 选项 | 类型 | 默认 | 说明 |
|---|---|---|---|
url | string | BUNNY_DATABASE_URL | libsql://, (中文(简体) ), https://,或 http:// 连接 URL. |
authToken | string | BUNNY_DATABASE_AUTH_TOKEN | 发送为 Authorization: Bearer <token>。 。 。 . |
fetch | typeof fetch | 全球 fetch | 代替测试、追踪或定制代理. |
headers | Record<string, string> | 无 | 每一个请求都要加头. |
signal | AbortSignal | 无 | 适用于每一个请求. |
timeout | number | 无 | 单项请求中止前的毫秒数 。 正整数可达2147483647. |
客户端重写 a libsql:// URL为 https://。它拒绝在 URL 中的证书,两者 user:pass@ 还有一个 authToken 查询参数,所以通过 authToken 换句话说 默默地放出一个符号 会让你调试一个无法解释的401.
每个请求都携带 User-Agent: bunny-database-client中页眉名称 headers 被匹配的大小写不敏感, 所以通过自己的 User-Agent 替换默认,而不是同时发送.
无 timeout 请求在运行时间允许的情况下等待,在边缘函数上意味着被挂起的取来可以将整个引用点烧掉. timeout 财务报告和审定财务报表 signal 作曲:以哪个起火先中止请求.
db.prepare(sql)
回返a Statement. 语句是不可改变的,所以你可以保持一个周围,并按你喜欢的频率绑住它.
const byId = db.prepare("SELECT * FROM users WHERE id = ?");
const alice = await byId.bind(1).first();
const bob = await byId.bind(2).first();
传出一行的字型,使其贯穿该语句的每次执行. 见 类型。 。 。 .
db.sql`...`
绑定每个内插值的模板文字,所以写出查询的最短方式也是参数化的:
const note = await db.sql`SELECT * FROM notes WHERE id = ${id}`.first();
每个 ${...} 变成一个 ? 占位符及其值是绑定的,从未被分解入 SQL 字符串。 它返回一个 Statement,所以所有的东西 执行 适用情况不变.
数值遵循与 bind(),有一个区别:插入对象抛出而不是被读取为被命名的参数,因为在模板中,它更有可能是一个错误.
用同样的方式通过一行 prepare(),作为 db.sql<User>`...` 。 。 。 .
只有值可以参数化,它是一个SQLite限制而不是客户端限制. 以 prepare() 当表格或列名必须更改时.
statement.bind(...values)
绑定参数并返回新语句。 接受 null, (中文(简体) ), boolean, (中文(简体) ), number, (中文(简体) ), bigint, (中文(简体) ), string,以及 Uint8Array。 。 。 .
传出值为 ? 占位符 :
await db.prepare("SELECT * FROM users WHERE id = ? AND active = ?").bind(1, true).all();
为 SQLite 的命名表单通过一个单一对象, :name, (中文(简体) ), @name,以及 $name数字 :
await db
.prepare("SELECT * FROM users WHERE id = :id AND active = :active")
.bind({ id: 1, active: true })
.all();
名字可能带了相片,或者把它留下,所以 { id: 1 } 财务报告和审定财务报表 { ":id": 1 } 两者都绑定 :id。一个语句使用一种样式:将位置值和对象混合在同一语句中 bind() 而不是为你挑一个赢家.
undefined 扔出而不是绑定 NULL,所以错误的属性(bind(user.nmae))表面而不是悄悄地写出NULL. 过 null 当你指NULL.
任何其它的投出,而不是被悄悄地转换,因为SQLite无处可放. Date 得到自己的信息建议 .toISOString() 或者说 .getTime()既然你猜到哪一个会改变 最终在栏目.
整数 numbers是作为INTEGER发送的,而它们完全合适,最高可达2^53. 过去的每双都是整数,所以客户端发送时是真实的,真实的存储值是不变的. 传出 a bigint 当你需要一个那么大的整数。 Bigints必须符合SQLite的64位签名范围.
执行
运行声明的四种方式:
const rows = await db.prepare("SELECT id, name FROM users").all();
// [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }]
const row = await db.prepare("SELECT * FROM users WHERE id = ?").bind(1).first();
// { id: 1, name: "Alice" } or null
const name = await db.prepare("SELECT name FROM users WHERE id = ?").bind(1).first("name");
// "Alice" or null
const rows = await db.prepare("SELECT id, name FROM users").raw();
// [[1, "Alice"], [2, "Bob"]]
run() 返回行加写入元数据 :
const result = await db
.prepare("INSERT INTO users (name) VALUES (?) RETURNING id")
.bind("Carol")
.run();
// {
// rows: [{ id: 3 }],
// columns: ["id"],
// rowsAffected: 1,
// lastInsertRowid: 3,
// }
runRaw() 返回与位置数组相同的有行的元数据。 当结果可能包含同名的两列时, 联系它, 因为对象行只保留上一列 :
const result = await db.prepare("SELECT a.id, b.id FROM a JOIN b").runRaw();
// { rows: [[1, 99]], columns: ["id", "id"], rowsAffected: 0, lastInsertRowid: null }
在其中之一被调用之前,声明什么都不做,所以 prepare() 财务报告和审定财务报表 bind() 安全地通过.
db.batch(statements, options?)
在一次交易和一次往返中运行每个语句。 他们都承诺或没有.
const [inserted, count] = await db.batch([
db.prepare("INSERT INTO users (name) VALUES (?)").bind("Dan"),
db.prepare("SELECT COUNT(*) AS c FROM users"),
]);
你得到一个 Result 按顺序排列. batchRaw() 相同但有位置行。 如果有声明失败, 交易会回滚 batch() 丢弃该语句错误, 与 error.batchIndex 设置到失败的语句位置.
批次是交易,所以你自己的声明开始 BEGIN, (中文(简体) ), COMMIT, (中文(简体) ), END,或 ROLLBACK 在发送任何信息之前被拒绝。 保存点是好的.
{ mode: "immediate" } 打开交易 BEGIN IMMEDIATE写字锁在前面。 SQLite的默认值, deferred,在首个写作时取下它,这样一批读取然后写作就会失败 SQLITE_BUSY 如果另一个作家进来了 使用 immediate 你认识的批次会写. exclusive 也予以接受.
batch() 从语句中推断出每个结果的行类型,所以 prepare<User>(...) 语句恢复为 Result<User> 乃至于无型相相相相. 见 类型。 。 。 .
{ foreignKeys: false } 将交易以 PRAGMA foreign_keys=off 财务报告和审定财务报表 =on。Schema 更改需要它: SQLite 的表格重建程序和若干 ALTER TABLE 形式要求真正取消执法,而不仅仅是延后实施. bunny db migrations apply 运行这边.
await db.batch(
[
db.prepare("CREATE TABLE users_new (id INTEGER PRIMARY KEY, email TEXT NOT NULL)"),
db.prepare("INSERT INTO users_new SELECT id, email FROM users"),
db.prepare("DROP TABLE users"),
db.prepare("ALTER TABLE users_new RENAME TO users"),
],
{ foreignKeys: false, mode: "immediate" },
);
保持这个顺序:以临时名称来构建替换,将原名放下后再重命名. 重新命名原版,让原版脱颖而出。 。 REFERENCES users 在其它表格中,要跟随重命名,它们最后指向一个你即将掉落的表格.
db.exec(sql)
运行多语句脚本。 它不需要参数,也不返回行,所以它主要用于设置一个计划.
await db.exec(`
CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT NOT NULL);
CREATE INDEX IF NOT EXISTS users_name ON users (name);
`);
任何你需要运行不止一次, 到达移民(bunny db migrations相反.
错误
一切抛出 DatabaseError数字 :
import { DatabaseError } from "@bunny.net/database-client";
try {
await db.prepare("INSERT INTO users (email) VALUES (?)").bind("[email protected]").run();
} catch (error) {
if (error instanceof DatabaseError && error.code === "SQLITE_CONSTRAINT") {
// handle the duplicate
}
}
| 财产 | 说明 |
|---|---|
message | 服务器的信息,或对本地验证失败的描述. |
code | SQLite 代码 (SQLITE_CONSTRAINT),或客户端代码(UNAUTHORIZED, (中文(简体) ), URL_MISSING) (中文(简体) ). |
status | HTTP状态,当故障来自运输而不是从SQL. |
batchIndex | 失败的语句的位置, 当您之一的语句在 batch() 失败 . |
在SQL运行之前发生的失败也被包住,所以一个 catch (error) { if (error instanceof DatabaseError) ... } 覆盖整个地表 而不是让一个 TypeError 通过:
code | 原因 |
|---|---|
NETWORK | DNS,TLS,连接拒绝,或者回应不是JSON. |
TIMEOUT | 这个 timeout 截止日期已过. |
ABORTED | 这个 signal 你通过了 流产. |
PROTOCOL | 服务器用一个不是管道反应的躯体回答200. |
对于这三人, error.cause 保存运行时间的原错误 .
类型
整数者回来 number 而他们完全合身 bigint 外加 Number.MAX_SAFE_INTEGER。客户端从不检查一个值使其适合.
| SQL 点 | Java 脚本 |
|---|---|
| 无效 | null |
| 进口 | number,或 bigint 历经2^53 |
| 真实 | number |
| 文本 | string |
| 块 | Uint8Array |
SQLite 没有布尔类型 . bind(true) 存储 1,并读回 1。 。 。 .
行为 Record<string, SqlValue> 默认。 传出自己的形状来跳过铸币局:
interface User {
id: number;
name: string;
}
const users = await db.prepare("SELECT id, name FROM users").all<User>();
或者一次输入语句,让其每次执行都继承形状:
const byId = db.prepare<User>("SELECT id, name FROM users WHERE id = ?");
const alice = await byId.bind(1).first(); // User | null
const both = await byId.bind(1).all(); // User[]
执行器上的一类参数仍然胜过声明,所以 all<Row>() 编号 Statement<User> 让你排回未打过的字.
这种类型是一种断言。 在运行时没有任何东西可以验证行与它相对,所以它只能像您的SQL那样准确.
边缘脚本
Edge脚本运行 Deno,因此可以从npm直接导入. 一个独立的脚本通过边框脚本SDK服务请求:
import * as BunnySDK from "npm:@bunny.net/[email protected]";
import { connect } from "npm:@bunny.net/database-client";
const db = connect();
BunnySDK.net.http.serve(async (request: Request): Promise<Response> => {
const users = await db.prepare("SELECT id, name FROM users LIMIT 10").all();
return Response.json(users);
});
在模块范围构建客户端是好的. connect() 打开无套接字和 I/O , 所以没有需要加热或拆卸的请求 .
Edge Script也是持有数据库符号的正确地点. 代码及其环境一直停留在兔子的边缘上,浏览器只看到了你选择返回的响应.
警卫
为什么没有浏览器
客户端只使用 fetch,所以它会很高兴运行 在浏览器。 它还是不应该去那里.
一个数据库认证令牌授权连接,这意味着任何持有它的东西都可以运行任何令牌允许的SQL对接任何表格. 在客户端代码中,符号出现在网络标签、JS捆绑、 localStorage,以及任何注入的脚本。 一旦它泄露,一个访客可以尽你所能, DROP TABLE 包含.
值得我们知道: bunny db tokens create 因此,你最有可能躺在周围的标志也是最糟糕的.
仅读的符号缩小了损坏而不修复:
bunny db tokens create --read-only --expiry 30d
这仍然把每张表格的每行都交出来,因为符号授权连接而不是查询,而SQLite没有行级的安全性可以回落.
怎么办呢
在您的服务器上保留此令牌, 只披露您实际想要允许的查询 。 通常指坐在数据库前的边缘脚本:
// Edge Script: the token stays here, the browser gets only this shape.
import * as BunnySDK from "npm:@bunny.net/[email protected]";
import { connect } from "npm:@bunny.net/database-client";
const db = connect();
BunnySDK.net.http.serve(async (request: Request): Promise<Response> => {
const url = new URL(request.url);
const author = url.searchParams.get("author");
if (!author) return new Response("author required", { status: 400 });
// Parameterized, and scoped to the columns and rows the caller may see.
const posts = await db
.prepare("SELECT id, title FROM posts WHERE author = ? AND published = 1 LIMIT 50")
.bind(author)
.all();
return Response.json(posts);
});
浏览器调用您的端点,您的端点决定了SQL的运行.
处理令牌
- 在环境变量中而不是在源中保留符号.
connect()读取BUNNY_DATABASE_URL财务报告和审定财务报表BUNNY_DATABASE_AUTH_TOKEN,所以一个符号 从来没有出现在代码. - 连接 URL 中的证书被拒绝,因为 URL 最终会出现在日志, 裁判员和错误报告中. 过
authToken换句话说. - 首选短寿活符(
bunny db tokens create --expiry 12h)和最狭小的授权有效. 如果有人漏掉的话,bunny db tokens invalidate取消数据库的每个符号. DatabaseError携带服务器的信息和SQLite代码,所以直接传递到客户端可以泄露出计划细节. 记录下来,然后还一些通用的.
发展
bun test
scripts/smoke.ts 运行客户端与两个运行时间的真实数据库。 它创造和放弃自己的桌子, 并留下任何东西:
bun run scripts/smoke.ts
deno run --env-file=.env --allow-net --allow-env scripts/smoke.ts