npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@dtdyq/restbase-client

v3.0.0

Published

Type-safe, zero-dependency client for RestBase API — works in Browser / Node / Bun / Deno

Readme

@dtdyq/restbase-client

零依赖 TypeScript 前端客户端,用于访问 RestBase REST API。兼容浏览器、Node.js、Bun、Deno。


安装

bun add @dtdyq/restbase-client
# 或
npm install @dtdyq/restbase-client
import 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 泛型参数 + 模板字面量类型 + 递归条件类型,在编译期自动推导查询结果类型 Sexec() / 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;     // 分页:总记录数
}