@dtdyq/restbase-client
v3.0.0
Published
Type-safe, zero-dependency client for RestBase API — works in Browser / Node / Bun / Deno
Maintainers
Readme
@dtdyq/restbase-client
零依赖 TypeScript 前端客户端,用于访问 RestBase REST API。兼容浏览器、Node.js、Bun、Deno。
安装
bun add @dtdyq/restbase-client
# 或
npm install @dtdyq/restbase-clientimport RestBase, {
eq, ne, gt, ge, lt, le,
isNull, isNotNull,
like, nlike,
isIn, notIn, between,
and, or,
sel, agg,
} from "@dtdyq/restbase-client";接口路由总览
| 接口 | 方法 | 说明 | 客户端方法 |
|:---------------------------|:-----------|:---------------|:------------------------------|
| /api/health | GET | 健康检查 | rb.health() |
| /api/auth/login | POST | 登录 | rb.auth.login() |
| /api/auth/register | POST | 注册 | rb.auth.register() |
| /api/auth/profile | GET | 获取用户资料 | rb.auth.getProfile() |
| /api/auth/profile | POST | 更新用户资料 | rb.auth.updateProfile() |
| /api/meta/tables | GET | 所有表元数据 | rb.tables() |
| /api/meta/tables/:name | GET | 单表元数据 | rb.tableMeta(name) |
| /api/meta/sync | GET | 同步表结构 | rb.syncMeta() |
| /api/query/:table | GET / POST | URL 查询 / Body 查询 | table.query().exec() |
| /api/query/:table/:pk | GET | 按主键获取 | table.get(pk) |
| /api/delete/:table | DELETE / POST | URL 删除 / Body 删除 | table.deleteWhere().exec() |
| /api/delete/:table/:pk | DELETE | 按主键删除 | table.delete(pk) |
| /api/update/:table | POST | 条件批量更新 | table.updateWhere().exec() |
| /api/save/:table | POST | 严格插入(存在报错) | table.insert(data) |
| /api/save/:table | PUT | Upsert(存在更新) | table.upsert(data) |
| /api/save/:table | PATCH | 严格更新(不存在报错) | table.update(data) |
快速开始
import RestBase, { eq, gt, or, agg, sel } from "@dtdyq/restbase-client";
// 同源部署 — 不需要传 endpoint
const rb = new RestBase();
// 或指定后端地址
// const rb = new RestBase("http://localhost:3333");
// JWT 登录(自动保存 token)
await rb.auth.login("admin", "admin");
// 查询
const products = rb.table("products");
const list = await products.query()
.where(gt("price", 100))
.orderDesc("price")
.page(1, 20)
.data();
// ── 多节点负载均衡 ──
// 所有节点连同一个数据库,请求随机分发
const rb2 = new RestBase([
"http://node1:3333",
"http://node2:3333",
"http://node3:3333",
]);
await rb2.auth.login("admin", "admin"); // 登录一次,token 共享
const list2 = await rb2.table("products").query().data(); // 随机打到某个节点RestBase — 主入口
// 同源部署(Vite proxy / 静态托管)
const rb = new RestBase();
// 单个 endpoint
const rb = new RestBase("http://localhost:3333");
// 多个 endpoint — 负载均衡(每次请求随机选一个节点)
const rb = new RestBase([
"http://localhost:3333",
"http://localhost:8080",
"http://localhost:9090",
]);多 endpoint 模式要求所有服务端实例连接同一个数据库。客户端共享同一套 auth 状态(token / Basic Auth),每次请求随机分发到不同节点,分散单节点压力。
| 方法 | 返回类型 | 说明 |
|:----------------------|:------------------------------------------|:------------------------------------|
| rb.auth | AuthClient | 鉴权客户端 |
| rb.table<T>(name) | TableClient<T> | 获取表操作客户端 |
| rb.health() | Promise<ApiResponse> | 健康检查(返回 name/port/pid/uptime/mem/cpu) |
| rb.tables() | Promise<ApiResponse<TableMeta[]>> | 获取所有表元数据(不含 users) |
| rb.tableMeta(name) | Promise<ApiResponse<TableMeta \| null>> | 获取单表元数据 |
| rb.syncMeta() | Promise<ApiResponse<TableMeta[]>> | 运行时同步 DB 表结构 |
| rb.setHeader(k, v) | this | 设置自定义请求头 |
| rb.setRequestId(id) | this | 设置请求追踪 ID(X-Request-Id) |
TableMeta 结构:
interface TableMeta {
name: string;
pk: string | null; // 主键名,无主键为 null
hasOwner: boolean; // 是否含 owner 字段(租户隔离)
columns: {
name: string;
type: string; // 数据库类型(如 "integer"、"text"、"real")
isNumeric: boolean;
}[];
}AuthClient — 鉴权
// 登录(成功后自动保存 JWT token)
const res = await rb.auth.login("admin", "password");
// res.data → JWT token 字符串
// 注册(成功后自动保存 JWT token)
await rb.auth.register("newuser", "password");
// 获取当前用户资料(去掉 id 和 password)
const profile = await rb.auth.getProfile();
// profile.data → { username: "admin", age: 26, ... }
// 泛型版本
const p = await rb.auth.getProfile<{ username: string; age: number }>();
// 更新资料(增量:仅传入要改的字段)
await rb.auth.updateProfile({ age: 27, email: "[email protected]" });
// Token 管理
rb.auth.setToken(savedToken); // 从 localStorage 恢复
rb.auth.getToken(); // 获取当前 token
rb.auth.logout(); // 清除 token
// 切换为 Basic Auth(每次请求携带用户名密码)
rb.auth.useBasicAuth("admin", "admin");TableClient — 表操作
// 无泛型:操作返回 Record<string, unknown>
const products = rb.table("products");
// 有泛型:获得完整类型提示
interface Product {
id: number;
name: string;
category: string;
price: number;
stock: number;
rating: number;
is_active: number;
}
const typedProducts = rb.table<Product>("products");| 方法 | 说明 | 返回 |
|:-------------------------|:--------------------------------------|:----------------------------------------------------------|
| table.query() | 创建链式查询(POST /api/query/:table) | QueryBuilder<T> |
| table.get(pk) | 按主键获取(GET /api/query/:table/:pk) | ApiResponse<T \| null> |
| table.insert(data) | 严格插入(POST /api/save/:table) | ApiResponse<{ created: unknown[] }> |
| table.upsert(data) | Upsert(PUT /api/save/:table) | ApiResponse<{ created: unknown[]; updated: unknown[] }> |
| table.update(data) | 严格更新(PATCH /api/save/:table) | ApiResponse<{ updated: unknown[] }> |
| table.delete(pk) | 按主键删除(DELETE /api/delete/:table/:pk) | ApiResponse<{ deleted: unknown[] }> |
| table.deleteWhere() | 创建链式条件删除(POST /api/delete/:table) | DeleteBuilder |
| table.updateWhere() | 创建链式条件更新(POST /api/update/:table) | UpdateBuilder |
CRUD 示例
// 按主键获取
const one = await products.get(42);
// one.data → Product | null
// 严格插入(存在同 PK 则报错 CONFLICT)
const cr = await products.insert({ name: "Widget", price: 29.9, stock: 100 });
// cr.data → { created: [101] } ← 新建记录的主键列表
// 批量插入
const batch = await products.insert([
{ name: "A", price: 10, stock: 50 },
{ name: "B", price: 20, stock: 30 },
]);
// batch.data → { created: [102, 103] }
// Upsert(不存在插入,存在增量更新)
const up = await products.upsert([{ id: 1, price: 88.8 }, { name: "New", price: 50 }]);
// up.data → { created: [104], updated: [1] } ← 区分新建与更新
// 严格更新(不存在报错 NOT_FOUND,必须带 PK)
const upd = await products.update({ id: 1, price: 99.9 });
// upd.data → { updated: [1] } ← 被更新的主键列表
// 按主键删除
const del = await products.delete(1);
// del.data → { deleted: [1] } ← 被删除的主键列表(未找到则 [])QueryBuilder — 链式查询
通过 table.query() 创建,内部使用 POST /api/query/:table,条件通过 JSON Body 传递。
const result = await products.query()
.where(gt("price", 50), lt("stock", 100))
.select("name", "price")
.orderDesc("price")
.page(1, 20)
.exec();.where(...conditions) — 添加 WHERE 条件
多次调用为 AND 关系。
// 简单条件
.where(eq("category", "Books"))
// 多条件(AND)
.where(gt("price", 10), lt("price", 100))
// 逻辑组合
.where(or(eq("category", "Books"), eq("category", "Toys")))
// 深度嵌套
.where(
or(
and(eq("category", "Electronics"), gt("price", 500)),
and(lt("stock", 10), eq("is_active", 1)),
)
).select(...fields) — 类型安全投影
select() 利用 TypeScript const 泛型参数 + 模板字面量类型 + 递归条件类型,在编译期自动推导查询结果类型 S。exec() / data() / first() 的返回类型都跟随 S 变化。
import { sel, agg } from "@dtdyq/restbase-client";
interface Product {
id: number; name: string; category: string;
price: number; stock: number; rating: number;
}
const products = rb.table<Product>("products");纯字段选择
// 返回类型: { name: string; price: number }[]
const a = await products.query()
.select("name", "price")
.data();
a[0].name; // ✅ string
a[0].price; // ✅ number
a[0].stock; // ❌ 编译错误 — 类型中不存在字段重命名 sel(field, alias)
// 返回类型: { unitPrice: number; name: string }[]
const b = await products.query()
.select(sel("price", "unitPrice"), "name")
.data();
b[0].unitPrice; // ✅ number(price 被映射为 unitPrice)聚合 agg(fn, field) / agg(fn, field, alias)
// 有 alias → 返回类型: { category: string; total: number }[]
const c = await products.query()
.select("category", agg("count", "id", "total"))
.groupBy("category")
.data();
c[0].total; // ✅ number
// 无 alias → 默认 key 为 "fn:field"
// 返回类型: { category: string; "count:id": number; "avg:price": number }[]
const d = await products.query()
.select("category", agg("count", "id"), agg("avg", "price"))
.groupBy("category")
.data();
d[0]["count:id"]; // ✅ number
d[0]["avg:price"]; // ✅ number聚合无 alias 时,客户端自动以
"fn:field"作为 alias 发送给服务端,服务端生成AS "fn:field"。类型侧使用模板字面量`${Fn}:${F}`保留。
字符串模板写法
// 字段:别名(field ∈ keyof T)→ { unitPrice: T["price"] }
.select("price:unitPrice")
// 函数:字段(func ∉ keyof T)→ { "count:id": number }
.select("count:id")
// 函数:字段:别名 → { total: number }
.select("count:id:total")类型推导规则汇总
| 参数格式 | 示例 | 推导结果 |
|:----------------------------|:----------------------------|:----------------------------|
| keyof T 字符串 | "name" | { name: T["name"] } |
| "func:field:alias" | "count:id:total" | { total: number } |
| "field:alias" (field ∈ T) | "price:unitPrice" | { unitPrice: T["price"] } |
| "func:field" (func ∉ T) | "count:id" | { "count:id": number } |
| sel(field) | sel("name") | { name: T["name"] } |
| sel(field, alias) | sel("price","up") | { up: T["price"] } |
| agg(fn, field) | agg("count","id") | { "count:id": number } |
| agg(fn, field, alias) | agg("count","id","total") | { total: number } |
.orderAsc() / .orderDesc() — 排序
.orderAsc("name") // ORDER BY name ASC
.orderDesc("price") // ORDER BY price DESC
.orderAsc("name").orderDesc("price") // 多字段.groupBy() — 分组
.groupBy("category")
.groupBy("category", "is_active") // 多字段.page(pageNo, pageSize) — 分页
.page(1, 20) // pageNo=1, pageSize=20
// 响应自动包含 total / pageNo / pageSize执行方法
| 方法 | 返回类型 | 说明 |
|:-----------|:----------------------------|:------------------------------|
| .exec() | Promise<ApiResponse<S[]>> | 完整响应(S 为 select 推导的投影类型,默认 T) |
| .data() | Promise<S[]> | 仅返回数据数组 |
| .first() | Promise<S \| null> | 返回第一条(自动 page(1,1)) |
| .build() | Record<string, unknown> | 构建请求 Body(不执行,用于调试) |
DeleteBuilder — 条件删除
通过 table.deleteWhere() 创建,内部使用 POST /api/delete/:table。
// 简单条件
const res = await products.deleteWhere()
.where(gt("price", 900))
.exec();
// res → { code: "OK", data: { deleted: [3, 7, 12] } } ← 被删除的主键列表
// 复杂条件
await products.deleteWhere()
.where(or(
gt("price", 900),
like("name", "%test%"),
))
.exec();
// 嵌套 AND + OR
await products.deleteWhere()
.where(and(
eq("is_active", 0),
or(lt("stock", 5), gt("price", 1000)),
))
.exec();UpdateBuilder — 条件批量更新
通过 table.updateWhere() 创建,内部使用 POST /api/update/:table。
// 单条件更新
const res = await products.updateWhere()
.set({ price: 99.99, stock: 100 })
.where(eq("category", "Books"))
.exec();
// res → { code: "OK", data: { updated: [3, 7, 12] } } ← 被更新的主键列表
// 复杂条件
await products.updateWhere()
.set({ is_active: 0 })
.where(and(
lt("stock", 5),
or(eq("category", "Toys"), eq("category", "Food")),
))
.exec();| 方法 | 说明 | 链式 |
|:---------------|:-------------|:---|
| .set(data) | 设置要更新的字段和值 | ✅ |
| .where(...c) | 添加 WHERE 条件 | ✅ |
| .exec() | 执行更新 | — |
安全机制:
where不允许为空(防止全表更新),set中的主键和 owner 字段会被自动忽略。
条件运算符速查
| 函数 | SQL | 示例 | 值类型 |
|:---------------------|:--------------|:----------------------------|:------------|
| eq(f, v) | = | eq("name", "test") | 标量 |
| ne(f, v) | != | ne("status", 0) | 标量 |
| gt(f, v) | > | gt("price", 100) | 标量 |
| ge(f, v) | >= | ge("age", 18) | 标量 |
| lt(f, v) | < | lt("stock", 10) | 标量 |
| le(f, v) | <= | le("rating", 3) | 标量 |
| isNull(f) | IS NULL | isNull("desc") | — |
| isNotNull(f) | IS NOT NULL | isNotNull("email") | — |
| like(f, p) | LIKE | like("name", "%test%") | 字符串(% 通配) |
| nlike(f, p) | NOT LIKE | nlike("name", "%x%") | 字符串 |
| isIn(f, arr) | IN (...) | isIn("id", [1, 2, 3]) | 数组 |
| notIn(f, arr) | NOT IN | notIn("status", [0]) | 数组 |
| between(f, lo, hi) | BETWEEN | between("price", 10, 100) | 两个标量 |
| and(...c) | AND | and(eq("a",1), gt("b",2)) | Condition[] |
| or(...c) | OR | or(eq("a",1), eq("a",2)) | Condition[] |
注意:Body 模式的 LIKE 直接使用 SQL
%通配符,不需要用*替换。
SELECT 辅助函数
| 函数 | 说明 | 示例 | 类型推导 |
|:------------------------|:------------------------|:------------------------------|:----------------------------|
| sel(field) | 选择字段 | sel("name") | { name: T["name"] } |
| sel(field, alias) | 字段重命名 | sel("price", "unitPrice") | { unitPrice: T["price"] } |
| agg(fn, field) | 聚合(alias 默认 fn:field) | agg("count", "id") | { "count:id": number } |
| agg(fn, field, alias) | 聚合 + 自定义别名 | agg("avg", "price", "avgP") | { avgP: number } |
支持的聚合函数:avg / max / min / count / sum
完整示例
import RestBase, {
eq, gt, lt, le, like, or, and, agg, sel, between, isIn,
} from "@dtdyq/restbase-client";
// 同源部署不传参,跨域传地址
const rb = new RestBase();
await rb.auth.login("admin", "admin");
// ── 元数据 ──
const allMeta = await rb.tables();
const prodMeta = await rb.tableMeta("products");
// ── 类型安全查询 ──
interface Product {
id: number; name: string; category: string;
price: number; stock: number; rating: number; is_active: number;
}
const products = rb.table<Product>("products");
// 搜索 → { name: string; price: number }[]
const search = await products.query()
.select("name", "price")
.where(like("name", "%Pro%"))
.data();
// 范围 + 排序 → Product[]
const filtered = await products.query()
.where(between("price", 100, 500), eq("is_active", 1))
.orderDesc("price")
.data();
// 分页 → 完整响应含 total
const page = await products.query()
.where(gt("stock", 0))
.page(2, 10)
.exec();
// 分组统计 → { category: string; total: number; avgPrice: number }[]
const stats = await products.query()
.select("category", agg("count", "id", "total"), agg("avg", "price", "avgPrice"))
.groupBy("category")
.data();
// 字段重命名 → { unitPrice: number }[]
const renamed = await products.query()
.select(sel("price", "unitPrice"))
.data();
// 复杂嵌套
const complex = await products.query()
.where(
or(
and(eq("category", "Electronics"), gt("price", 500)),
and(lt("stock", 10), eq("is_active", 1)),
),
)
.data();
// 第一条
const first = await products.query()
.where(gt("rating", 4))
.orderDesc("rating")
.first();
// ── 条件删除 ──
await products.deleteWhere()
.where(le("rating", 1))
.exec();
// ── 条件更新 ──
await products.updateWhere()
.set({ price: 99.99 })
.where(eq("category", "Books"))
.exec();
// ── CRUD ──
await products.insert({ name: "New", price: 50, stock: 100 });
await products.insert([{ name: "A", price: 10 }, { name: "B", price: 20 }]);
await products.upsert({ id: 1, price: 88 });
await products.update({ id: 1, stock: 200 });
await products.delete(99);
const one = await products.get(42);错误处理
所有接口统一返回 HTTP 200 + JSON,通过 code 判断成功/失败:
const res = await rb.auth.login("wrong", "password");
if (res.code !== "OK") {
console.error(`[${res.code}] ${res.message}`);
// [AUTH_ERROR] Invalid username or password
}| code | 说明 |
|:-------------------|:-----------------------------|
| OK | 成功 |
| AUTH_ERROR | 鉴权失败(未登录/密码错误/Token过期/用户已存在) |
| VALIDATION_ERROR | 请求体校验失败 |
| NOT_FOUND | 表不存在 |
| CONFLICT | 记录已存在(主键冲突) |
| TABLE_ERROR | 表无主键(不支持按 ID 操作) |
| FORBIDDEN | 禁止操作用户表 |
| QUERY_ERROR | 查询语法错误 |
| RATE_LIMITED | API 请求频率超限 |
| SYS_ERROR | 系统异常 |
ApiResponse 类型
interface ApiResponse<T = unknown> {
code: string; // "OK" 或错误码
message?: string; // 错误详情
data: T; // 业务数据
pageNo?: number; // 分页:当前页
pageSize?: number; // 分页:每页条数
total?: number; // 分页:总记录数
}