@dtdyq/restbase 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,11 +15,40 @@
15
15
 
16
16
  ## 快速开始
17
17
 
18
+ ### 方式一:全局安装(推荐)
19
+
20
+ ```bash
21
+ bun install -g @dtdyq/restbase
22
+ ```
23
+
24
+ 安装后直接在任意目录使用 `restbase` 命令:
25
+
26
+ ```bash
27
+ # 默认 SQLite 内存模式启动
28
+ restbase
29
+
30
+ # 指定数据库和端口
31
+ DB_URL=sqlite://./data.db SVR_PORT=8080 restbase
32
+
33
+ # 连接 MySQL
34
+ DB_URL=mysql://user:pass@localhost/mydb restbase
35
+ ```
36
+
37
+ 更新到最新版:
38
+
39
+ ```bash
40
+ bun install -g @dtdyq/restbase@latest
41
+ ```
42
+
43
+ ### 方式二:从源码运行
44
+
18
45
  ```bash
19
46
  bun install
20
47
  bun run server.ts
21
48
  ```
22
49
 
50
+ ### 验证
51
+
23
52
  ```bash
24
53
  # 健康检查
25
54
  curl http://localhost:3333/api/health
@@ -28,6 +57,24 @@ curl http://localhost:3333/api/health
28
57
  curl -u admin:admin http://localhost:3333/api/data/products
29
58
  ```
30
59
 
60
+ ### 前端客户端
61
+
62
+ 独立 npm 包,零依赖:
63
+
64
+ ```bash
65
+ bun add @dtdyq/restbase-client
66
+ ```
67
+
68
+ ```ts
69
+ import RestBase, { gt } from "@dtdyq/restbase-client";
70
+
71
+ const rb = new RestBase(); // 同源部署不传参
72
+ await rb.auth.login("admin", "admin");
73
+ const data = await rb.table("products").query().where(gt("price", 100)).data();
74
+ ```
75
+
76
+ 详见 [client/README.md](client/README.md)。
77
+
31
78
  ## 构建与部署
32
79
 
33
80
  ```bash
@@ -73,7 +120,7 @@ bun test rest.test.ts # 137+ 用例
73
120
  | 文档 | 内容 |
74
121
  |:-------------------------------------------------|:--------------------------------------------------|
75
122
  | [documents/server.md](documents/server.md) | 服务端详细文档 — 配置、全部 API 接口说明与示例、日志、部署 |
76
- | [documents/client.md](documents/client.md) | 前端客户端文档 — 安装、API 速查、QueryBuilder 链式调用、类型安全 SELECT |
123
+ | [client/README.md](client/README.md) | 前端客户端文档 — 安装、API 速查、QueryBuilder 链式调用、类型安全 SELECT |
77
124
  | [documents/db_design.md](documents/db_design.md) | 数据库设计指南 — 表结构规范、约束、索引、设计模式与检查清单 |
78
125
  | [documents/design.md](documents/design.md) | 需求与设计文档 — 架构设计、技术规格、完整接口定义 |
79
126
 
@@ -81,19 +128,23 @@ bun test rest.test.ts # 137+ 用例
81
128
 
82
129
  ```
83
130
  restbase/
84
- ├── server.ts # 入口
85
- ├── types.ts # 配置 + 类型
131
+ ├── bin/
132
+ │ └── restbase.ts # 全局命令入口(bun install -g)
133
+ ├── server.ts # 服务入口
134
+ ├── types.ts # 配置 + 类型 + Zod Schema
86
135
  ├── db.ts # 数据库
87
136
  ├── auth.ts # 鉴权
88
137
  ├── crud.ts # CRUD 路由
89
138
  ├── query.ts # SQL 生成
90
139
  ├── logger.ts # 日志
91
140
  ├── client/
92
- └── restbase-client.ts # 前端客户端
141
+ ├── restbase-client.ts # 前端客户端(独立 npm 包 @dtdyq/restbase-client)
142
+ │ ├── README.md # 客户端文档
143
+ │ └── package.json
93
144
  ├── documents/
94
145
  │ ├── design.md # 需求设计文档
95
146
  │ ├── server.md # 服务端文档
96
- │ └── client.md # 客户端文档
147
+ │ └── db_design.md # 数据库设计指南
97
148
  ├── init.sql # 初始化 SQL
98
149
  ├── rest.test.ts # 集成测试
99
150
  ├── .env / .env.test # 环境配置
@@ -0,0 +1,541 @@
1
+ # @dtdyq/restbase-client
2
+
3
+ > 零依赖 TypeScript 前端客户端,用于访问 RestBase REST API。兼容浏览器、Node.js、Bun、Deno。
4
+
5
+ ---
6
+
7
+ ## 安装
8
+
9
+ ```bash
10
+ bun add @dtdyq/restbase-client
11
+ # 或
12
+ npm install @dtdyq/restbase-client
13
+ ```
14
+
15
+ ```ts
16
+ import RestBase, {
17
+ eq, ne, gt, ge, lt, le,
18
+ isNull, isNotNull,
19
+ like, nlike,
20
+ isIn, notIn, between,
21
+ and, or,
22
+ sel, agg,
23
+ } from "@dtdyq/restbase-client";
24
+ ```
25
+
26
+ ---
27
+
28
+ ## 接口路由总览
29
+
30
+ | 接口 | 方法 | 说明 | 客户端方法 |
31
+ |:-------------------------|:---------|:------------------|:-----------------------------|
32
+ | `/api/health` | GET | 健康检查 | `rb.health()` |
33
+ | `/api/auth/login` | POST | 登录 | `rb.auth.login()` |
34
+ | `/api/auth/register` | POST | 注册 | `rb.auth.register()` |
35
+ | `/api/auth/profile` | GET | 获取用户资料 | `rb.auth.getProfile()` |
36
+ | `/api/auth/profile` | POST | 更新用户资料 | `rb.auth.updateProfile()` |
37
+ | `/api/meta/tables` | GET | 所有表元数据 | `rb.tables()` |
38
+ | `/api/meta/tables/:name` | GET | 单表元数据 | `rb.tableMeta(name)` |
39
+ | `/api/meta/sync` | GET | 同步表结构 | `rb.syncMeta()` |
40
+ | `/api/query/:table` | **POST** | **Body 查询(前端推荐)** | `table.query().exec()` |
41
+ | `/api/delete/:table` | **POST** | **Body 条件删除** | `table.deleteWhere().exec()` |
42
+ | `/api/data/:table` | POST | 创建 | `table.create()` |
43
+ | `/api/data/:table` | PUT | Upsert | `table.put()` |
44
+ | `/api/data/:table/:id` | GET | 按主键获取 | `table.getByPk()` |
45
+ | `/api/data/:table/:id` | DELETE | 按主键删除 | `table.deleteByPk()` |
46
+
47
+ ---
48
+
49
+ ## 快速开始
50
+
51
+ ```ts
52
+ import RestBase, { eq, gt, or, agg, sel } from "@dtdyq/restbase-client";
53
+
54
+ // 同源部署(Vite proxy / 静态托管)— 不需要传 endpoint
55
+ const rb = new RestBase();
56
+
57
+ // 或指定后端地址
58
+ // const rb = new RestBase("http://localhost:3333");
59
+
60
+ // JWT 登录(自动保存 token)
61
+ await rb.auth.login("admin", "admin");
62
+
63
+ // 查询
64
+ const products = rb.table("products");
65
+ const list = await products.query()
66
+ .where(gt("price", 100))
67
+ .orderDesc("price")
68
+ .page(1, 20)
69
+ .data();
70
+ ```
71
+
72
+ ---
73
+
74
+ ## RestBase — 主入口
75
+
76
+ ```ts
77
+ const rb = new RestBase(); // 同源
78
+ const rb = new RestBase("http://localhost:3333"); // 跨域
79
+ ```
80
+
81
+ | 方法 | 返回类型 | 说明 |
82
+ |:----------------------|:------------------------------------------|:--------------------------|
83
+ | `rb.auth` | `AuthClient` | 鉴权客户端 |
84
+ | `rb.table<T>(name)` | `TableClient<T>` | 获取表操作客户端(可指定泛型) |
85
+ | `rb.health()` | `Promise<ApiResponse>` | 健康检查 |
86
+ | `rb.tables()` | `Promise<ApiResponse<TableMeta[]>>` | 获取所有表元数据(不含 users) |
87
+ | `rb.tableMeta(name)` | `Promise<ApiResponse<TableMeta \| null>>` | 获取单表元数据(不存在返回 null) |
88
+ | `rb.syncMeta()` | `Promise<ApiResponse<TableMeta[]>>` | 运行时同步 DB 表结构 |
89
+ | `rb.setHeader(k, v)` | `this` | 设置自定义请求头 |
90
+ | `rb.setRequestId(id)` | `this` | 设置请求追踪 ID(`X-Request-Id`) |
91
+
92
+ **TableMeta 结构:**
93
+
94
+ ```ts
95
+ interface TableMeta {
96
+ name: string;
97
+ pk: string | null; // 主键名,无主键为 null
98
+ hasOwner: boolean; // 是否含 owner 字段(租户隔离)
99
+ columns: {
100
+ name: string;
101
+ type: string; // 数据库类型(如 "integer"、"text"、"real")
102
+ isNumeric: boolean;
103
+ }[];
104
+ }
105
+ ```
106
+
107
+ ---
108
+
109
+ ## AuthClient — 鉴权
110
+
111
+ ```ts
112
+ // 登录(成功后自动保存 JWT token)
113
+ const res = await rb.auth.login("admin", "password");
114
+ // res.data → JWT token 字符串
115
+
116
+ // 注册(成功后自动保存 JWT token)
117
+ await rb.auth.register("newuser", "password");
118
+
119
+ // 获取当前用户资料(去掉 id 和 password)
120
+ const profile = await rb.auth.getProfile();
121
+ // profile.data → { username: "admin", age: 26, ... }
122
+
123
+ // 泛型版本
124
+ const p = await rb.auth.getProfile<{ username: string; age: number }>();
125
+
126
+ // 更新资料(增量:仅传入要改的字段)
127
+ await rb.auth.updateProfile({ age: 27, email: "new@example.com" });
128
+
129
+ // Token 管理
130
+ rb.auth.setToken(savedToken); // 从 localStorage 恢复
131
+ rb.auth.getToken(); // 获取当前 token
132
+ rb.auth.logout(); // 清除 token
133
+
134
+ // 切换为 Basic Auth(每次请求携带用户名密码)
135
+ rb.auth.useBasicAuth("admin", "admin");
136
+ ```
137
+
138
+ ---
139
+
140
+ ## TableClient — 表操作
141
+
142
+ ```ts
143
+ // 无泛型:操作返回 Record<string, unknown>
144
+ const products = rb.table("products");
145
+
146
+ // 有泛型:获得完整类型提示
147
+ interface Product {
148
+ id: number;
149
+ name: string;
150
+ category: string;
151
+ price: number;
152
+ stock: number;
153
+ rating: number;
154
+ is_active: number;
155
+ }
156
+ const typedProducts = rb.table<Product>("products");
157
+ ```
158
+
159
+ | 方法 | 说明 | 返回 |
160
+ |:-----------------------|:-----------------------------------|:----------------------------------------------------------|
161
+ | `table.query()` | 创建链式查询(POST /api/query/:table) | `QueryBuilder<T>` |
162
+ | `table.getByPk(id)` | 按主键获取(GET /api/data/:table/:id) | `ApiResponse<T \| null>` |
163
+ | `table.create(data)` | 创建(POST /api/data/:table) | `ApiResponse<{ created: unknown[] }>` |
164
+ | `table.put(data)` | Upsert(PUT /api/data/:table) | `ApiResponse<{ created: unknown[]; updated: unknown[] }>` |
165
+ | `table.deleteByPk(id)` | 按主键删除(DELETE /api/data/:table/:id) | `ApiResponse<{ deleted: unknown[] }>` |
166
+ | `table.deleteWhere()` | 创建链式条件删除(POST /api/delete/:table) | `DeleteBuilder` |
167
+
168
+ ### CRUD 示例
169
+
170
+ ```ts
171
+ // 按主键获取
172
+ const one = await products.getByPk(42);
173
+ // one.data → Product | null
174
+
175
+ // 创建(单条)
176
+ const cr = await products.create({ name: "Widget", price: 29.9, stock: 100 });
177
+ // cr.data → { created: [101] } ← 新建记录的主键列表
178
+
179
+ // 创建(批量)
180
+ const batch = await products.create([
181
+ { name: "A", price: 10, stock: 50 },
182
+ { name: "B", price: 20, stock: 30 },
183
+ ]);
184
+ // batch.data → { created: [102, 103] }
185
+
186
+ // Upsert(不存在创建,存在增量更新)
187
+ const up = await products.put([{ id: 1, price: 88.8 }, { name: "New", price: 50 }]);
188
+ // up.data → { created: [104], updated: [1] } ← 区分新建与更新
189
+
190
+ // 按主键删除
191
+ const del = await products.deleteByPk(1);
192
+ // del.data → { deleted: [1] } ← 被删除的主键列表(未找到则 [])
193
+ ```
194
+
195
+ ---
196
+
197
+ ## QueryBuilder — 链式查询
198
+
199
+ 通过 `table.query()` 创建,内部使用 `POST /api/query/:table`,条件通过 JSON Body 传递。
200
+
201
+ ```ts
202
+ const result = await products.query()
203
+ .where(gt("price", 50), lt("stock", 100))
204
+ .select("name", "price")
205
+ .orderDesc("price")
206
+ .page(1, 20)
207
+ .exec();
208
+ ```
209
+
210
+ ### `.where(...conditions)` — 添加 WHERE 条件
211
+
212
+ 多次调用为 AND 关系。
213
+
214
+ ```ts
215
+ // 简单条件
216
+ .where(eq("category", "Books"))
217
+
218
+ // 多条件(AND)
219
+ .where(gt("price", 10), lt("price", 100))
220
+
221
+ // 逻辑组合
222
+ .where(or(eq("category", "Books"), eq("category", "Toys")))
223
+
224
+ // 深度嵌套
225
+ .where(
226
+ or(
227
+ and(eq("category", "Electronics"), gt("price", 500)),
228
+ and(lt("stock", 10), eq("is_active", 1)),
229
+ )
230
+ )
231
+ ```
232
+
233
+ ### `.select(...fields)` — 类型安全投影
234
+
235
+ `select()` 利用 TypeScript `const` 泛型参数 + 模板字面量类型 + 递归条件类型,在**编译期**自动推导查询结果类型 `S`。`exec()` / `data()` / `first()` 的返回类型都跟随 `S` 变化。
236
+
237
+ ```ts
238
+ import { sel, agg } from "@dtdyq/restbase-client";
239
+
240
+ interface Product {
241
+ id: number; name: string; category: string;
242
+ price: number; stock: number; rating: number;
243
+ }
244
+ const products = rb.table<Product>("products");
245
+ ```
246
+
247
+ #### 纯字段选择
248
+
249
+ ```ts
250
+ // 返回类型: { name: string; price: number }[]
251
+ const a = await products.query()
252
+ .select("name", "price")
253
+ .data();
254
+
255
+ a[0].name; // ✅ string
256
+ a[0].price; // ✅ number
257
+ a[0].stock; // ❌ 编译错误 — 类型中不存在
258
+ ```
259
+
260
+ #### 字段重命名 `sel(field, alias)`
261
+
262
+ ```ts
263
+ // 返回类型: { unitPrice: number; name: string }[]
264
+ const b = await products.query()
265
+ .select(sel("price", "unitPrice"), "name")
266
+ .data();
267
+
268
+ b[0].unitPrice; // ✅ number(price 被映射为 unitPrice)
269
+ ```
270
+
271
+ #### 聚合 `agg(fn, field)` / `agg(fn, field, alias)`
272
+
273
+ ```ts
274
+ // 有 alias → 返回类型: { category: string; total: number }[]
275
+ const c = await products.query()
276
+ .select("category", agg("count", "id", "total"))
277
+ .groupBy("category")
278
+ .data();
279
+
280
+ c[0].total; // ✅ number
281
+
282
+ // 无 alias → 默认 key 为 "fn:field"
283
+ // 返回类型: { category: string; "count:id": number; "avg:price": number }[]
284
+ const d = await products.query()
285
+ .select("category", agg("count", "id"), agg("avg", "price"))
286
+ .groupBy("category")
287
+ .data();
288
+
289
+ d[0]["count:id"]; // ✅ number
290
+ d[0]["avg:price"]; // ✅ number
291
+ ```
292
+
293
+ > **聚合无 alias 时**,客户端自动以 `"fn:field"` 作为 alias 发送给服务端,服务端生成 `AS "fn:field"`。类型侧使用模板字面量 `` `${Fn}:${F}` `` 保留。
294
+
295
+ #### 字符串模板写法
296
+
297
+ ```ts
298
+ // 字段:别名(field ∈ keyof T)→ { unitPrice: T["price"] }
299
+ .select("price:unitPrice")
300
+
301
+ // 函数:字段(func ∉ keyof T)→ { "count:id": number }
302
+ .select("count:id")
303
+
304
+ // 函数:字段:别名 → { total: number }
305
+ .select("count:id:total")
306
+ ```
307
+
308
+ #### 类型推导规则汇总
309
+
310
+ | 参数格式 | 示例 | 推导结果 |
311
+ |:----------------------------|:----------------------------|:----------------------------|
312
+ | `keyof T` 字符串 | `"name"` | `{ name: T["name"] }` |
313
+ | `"func:field:alias"` | `"count:id:total"` | `{ total: number }` |
314
+ | `"field:alias"` (field ∈ T) | `"price:unitPrice"` | `{ unitPrice: T["price"] }` |
315
+ | `"func:field"` (func ∉ T) | `"count:id"` | `{ "count:id": number }` |
316
+ | `sel(field)` | `sel("name")` | `{ name: T["name"] }` |
317
+ | `sel(field, alias)` | `sel("price","up")` | `{ up: T["price"] }` |
318
+ | `agg(fn, field)` | `agg("count","id")` | `{ "count:id": number }` |
319
+ | `agg(fn, field, alias)` | `agg("count","id","total")` | `{ total: number }` |
320
+
321
+ ### `.orderAsc()` / `.orderDesc()` — 排序
322
+
323
+ ```ts
324
+ .orderAsc("name") // ORDER BY name ASC
325
+ .orderDesc("price") // ORDER BY price DESC
326
+ .orderAsc("name").orderDesc("price") // 多字段
327
+ ```
328
+
329
+ ### `.groupBy()` — 分组
330
+
331
+ ```ts
332
+ .groupBy("category")
333
+ .groupBy("category", "is_active") // 多字段
334
+ ```
335
+
336
+ ### `.page(pageNo, pageSize)` — 分页
337
+
338
+ ```ts
339
+ .page(1, 20) // pageNo=1, pageSize=20
340
+ // 响应自动包含 total / pageNo / pageSize
341
+ ```
342
+
343
+ ### 执行方法
344
+
345
+ | 方法 | 返回类型 | 说明 |
346
+ |:-----------|:----------------------------|:------------------------------|
347
+ | `.exec()` | `Promise<ApiResponse<S[]>>` | 完整响应(S 为 select 推导的投影类型,默认 T) |
348
+ | `.data()` | `Promise<S[]>` | 仅返回数据数组 |
349
+ | `.first()` | `Promise<S \| null>` | 返回第一条(自动 `page(1,1)`) |
350
+ | `.build()` | `Record<string, unknown>` | 构建请求 Body(不执行,用于调试) |
351
+
352
+ ---
353
+
354
+ ## DeleteBuilder — 条件删除
355
+
356
+ 通过 `table.deleteWhere()` 创建,内部使用 `POST /api/delete/:table`。
357
+
358
+ ```ts
359
+ // 简单条件
360
+ const res = await products.deleteWhere()
361
+ .where(gt("price", 900))
362
+ .exec();
363
+ // res → { code: "OK", data: { deleted: [3, 7, 12] } } ← 被删除的主键列表
364
+
365
+ // 复杂条件
366
+ await products.deleteWhere()
367
+ .where(or(
368
+ gt("price", 900),
369
+ like("name", "%test%"),
370
+ ))
371
+ .exec();
372
+
373
+ // 嵌套 AND + OR
374
+ await products.deleteWhere()
375
+ .where(and(
376
+ eq("is_active", 0),
377
+ or(lt("stock", 5), gt("price", 1000)),
378
+ ))
379
+ .exec();
380
+ ```
381
+
382
+ ---
383
+
384
+ ## 条件运算符速查
385
+
386
+ | 函数 | SQL | 示例 | 值类型 |
387
+ |:---------------------|:--------------|:----------------------------|:------------|
388
+ | `eq(f, v)` | `=` | `eq("name", "test")` | 标量 |
389
+ | `ne(f, v)` | `!=` | `ne("status", 0)` | 标量 |
390
+ | `gt(f, v)` | `>` | `gt("price", 100)` | 标量 |
391
+ | `ge(f, v)` | `>=` | `ge("age", 18)` | 标量 |
392
+ | `lt(f, v)` | `<` | `lt("stock", 10)` | 标量 |
393
+ | `le(f, v)` | `<=` | `le("rating", 3)` | 标量 |
394
+ | `isNull(f)` | `IS NULL` | `isNull("desc")` | — |
395
+ | `isNotNull(f)` | `IS NOT NULL` | `isNotNull("email")` | — |
396
+ | `like(f, p)` | `LIKE` | `like("name", "%test%")` | 字符串(`%` 通配) |
397
+ | `nlike(f, p)` | `NOT LIKE` | `nlike("name", "%x%")` | 字符串 |
398
+ | `isIn(f, arr)` | `IN (...)` | `isIn("id", [1, 2, 3])` | 数组 |
399
+ | `notIn(f, arr)` | `NOT IN` | `notIn("status", [0])` | 数组 |
400
+ | `between(f, lo, hi)` | `BETWEEN` | `between("price", 10, 100)` | 两个标量 |
401
+ | `and(...c)` | `AND` | `and(eq("a",1), gt("b",2))` | Condition[] |
402
+ | `or(...c)` | `OR` | `or(eq("a",1), eq("a",2))` | Condition[] |
403
+
404
+ > **注意**:Body 模式的 LIKE 直接使用 SQL `%` 通配符,不需要用 `*` 替换。
405
+
406
+ ---
407
+
408
+ ## SELECT 辅助函数
409
+
410
+ | 函数 | 说明 | 示例 | 类型推导 |
411
+ |:------------------------|:------------------------|:------------------------------|:----------------------------|
412
+ | `sel(field)` | 选择字段 | `sel("name")` | `{ name: T["name"] }` |
413
+ | `sel(field, alias)` | 字段重命名 | `sel("price", "unitPrice")` | `{ unitPrice: T["price"] }` |
414
+ | `agg(fn, field)` | 聚合(alias 默认 `fn:field`) | `agg("count", "id")` | `{ "count:id": number }` |
415
+ | `agg(fn, field, alias)` | 聚合 + 自定义别名 | `agg("avg", "price", "avgP")` | `{ avgP: number }` |
416
+
417
+ 支持的聚合函数:`avg` / `max` / `min` / `count` / `sum`
418
+
419
+ ---
420
+
421
+ ## 完整示例
422
+
423
+ ```ts
424
+ import RestBase, {
425
+ eq, gt, lt, le, like, or, and, agg, sel, between, isIn,
426
+ } from "@dtdyq/restbase-client";
427
+
428
+ // 同源部署不传参,跨域传地址
429
+ const rb = new RestBase();
430
+ await rb.auth.login("admin", "admin");
431
+
432
+ // ── 元数据 ──
433
+ const allMeta = await rb.tables();
434
+ const prodMeta = await rb.tableMeta("products");
435
+
436
+ // ── 类型安全查询 ──
437
+ interface Product {
438
+ id: number; name: string; category: string;
439
+ price: number; stock: number; rating: number; is_active: number;
440
+ }
441
+ const products = rb.table<Product>("products");
442
+
443
+ // 搜索 → { name: string; price: number }[]
444
+ const search = await products.query()
445
+ .select("name", "price")
446
+ .where(like("name", "%Pro%"))
447
+ .data();
448
+
449
+ // 范围 + 排序 → Product[]
450
+ const filtered = await products.query()
451
+ .where(between("price", 100, 500), eq("is_active", 1))
452
+ .orderDesc("price")
453
+ .data();
454
+
455
+ // 分页 → 完整响应含 total
456
+ const page = await products.query()
457
+ .where(gt("stock", 0))
458
+ .page(2, 10)
459
+ .exec();
460
+
461
+ // 分组统计 → { category: string; total: number; avgPrice: number }[]
462
+ const stats = await products.query()
463
+ .select("category", agg("count", "id", "total"), agg("avg", "price", "avgPrice"))
464
+ .groupBy("category")
465
+ .data();
466
+
467
+ // 字段重命名 → { unitPrice: number }[]
468
+ const renamed = await products.query()
469
+ .select(sel("price", "unitPrice"))
470
+ .data();
471
+
472
+ // 复杂嵌套
473
+ const complex = await products.query()
474
+ .where(
475
+ or(
476
+ and(eq("category", "Electronics"), gt("price", 500)),
477
+ and(lt("stock", 10), eq("is_active", 1)),
478
+ ),
479
+ )
480
+ .data();
481
+
482
+ // 第一条
483
+ const first = await products.query()
484
+ .where(gt("rating", 4))
485
+ .orderDesc("rating")
486
+ .first();
487
+
488
+ // ── 条件删除 ──
489
+ await products.deleteWhere()
490
+ .where(le("rating", 1))
491
+ .exec();
492
+
493
+ // ── CRUD ──
494
+ await products.create({ name: "New", price: 50, stock: 100 });
495
+ await products.create([{ name: "A", price: 10 }, { name: "B", price: 20 }]);
496
+ await products.put({ id: 1, price: 88 });
497
+ await products.deleteByPk(99);
498
+ const one = await products.getByPk(42);
499
+ ```
500
+
501
+ ---
502
+
503
+ ## 错误处理
504
+
505
+ 所有接口统一返回 HTTP 200 + JSON,通过 `code` 判断成功/失败:
506
+
507
+ ```ts
508
+ const res = await rb.auth.login("wrong", "password");
509
+ if (res.code !== "OK") {
510
+ console.error(`[${res.code}] ${res.message}`);
511
+ // [AUTH_ERROR] Invalid username or password
512
+ }
513
+ ```
514
+
515
+ | code | 说明 |
516
+ |:-------------------|:-----------------------------|
517
+ | `OK` | 成功 |
518
+ | `AUTH_ERROR` | 鉴权失败(未登录/密码错误/Token过期/用户已存在) |
519
+ | `VALIDATION_ERROR` | 请求体校验失败 |
520
+ | `NOT_FOUND` | 表不存在 |
521
+ | `CONFLICT` | 记录已存在(主键冲突) |
522
+ | `TABLE_ERROR` | 表无主键(不支持按 ID 操作) |
523
+ | `FORBIDDEN` | 禁止操作用户表 |
524
+ | `QUERY_ERROR` | 查询语法错误 |
525
+ | `RATE_LIMITED` | API 请求频率超限 |
526
+ | `SYS_ERROR` | 系统异常 |
527
+
528
+ ---
529
+
530
+ ## ApiResponse 类型
531
+
532
+ ```ts
533
+ interface ApiResponse<T = unknown> {
534
+ code: string; // "OK" 或错误码
535
+ message?: string; // 错误详情
536
+ data: T; // 业务数据
537
+ pageNo?: number; // 分页:当前页
538
+ pageSize?: number; // 分页:每页条数
539
+ total?: number; // 分页:总记录数
540
+ }
541
+ ```
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dtdyq/restbase-client",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Type-safe, zero-dependency client for RestBase API — works in Browser / Node / Bun / Deno",
5
5
  "keywords": ["restbase", "rest", "api", "client", "typescript", "query-builder"],
6
6
  "license": "MIT",
@@ -17,6 +17,7 @@
17
17
  }
18
18
  },
19
19
  "files": [
20
- "restbase-client.ts"
20
+ "restbase-client.ts",
21
+ "README.md"
21
22
  ]
22
23
  }
@@ -530,7 +530,7 @@ export class RestBase {
530
530
  readonly auth: AuthClient;
531
531
  private _http: HttpClient;
532
532
 
533
- constructor(endpoint: string) {
533
+ constructor(endpoint: string = "") {
534
534
  this._http = new HttpClient(endpoint);
535
535
  this.auth = new AuthClient(this._http);
536
536
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dtdyq/restbase",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Zero-code REST API server for SQLite / MySQL with built-in auth, tenant isolation, and TypeScript client",
5
5
  "keywords": ["rest", "api", "crud", "sqlite", "mysql", "hono", "bun", "zero-code"],
6
6
  "license": "MIT",
package/server.ts CHANGED
@@ -26,6 +26,7 @@ import {log} from "./logger.ts";
26
26
  import {getTableMetaByName, getTablesMeta, initDb, syncTablesMeta} from "./db.ts";
27
27
  import {authMiddleware, registerAuthRoutes} from "./auth.ts";
28
28
  import {registerCrudRoutes} from "./crud.ts";
29
+ import pkg from "./package.json";
29
30
 
30
31
  /* ═══════════ 1. 安全检查 ═══════════ */
31
32
 
@@ -198,4 +199,4 @@ if (cfg.staticDir) {
198
199
  /* ═══════════ 13. 启动服务 ═══════════ */
199
200
 
200
201
  export const server = Bun.serve({port: cfg.port, fetch: app.fetch});
201
- log.info({port: cfg.port}, `Server started http://localhost:${cfg.port}`);
202
+ log.info({version: pkg.version, port: cfg.port}, `RestBase v${pkg.version} started http://localhost:${cfg.port}`);