@dtdyq/restbase 1.0.0
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 +105 -0
- package/auth.ts +164 -0
- package/bin/restbase.ts +2 -0
- package/client/package.json +22 -0
- package/client/restbase-client.ts +576 -0
- package/crud.ts +329 -0
- package/db.ts +197 -0
- package/logger.ts +94 -0
- package/package.json +50 -0
- package/query.ts +580 -0
- package/server.ts +201 -0
- package/types.ts +186 -0
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* restbase-client.ts — RestBase 前端客户端
|
|
3
|
+
*
|
|
4
|
+
* 零依赖、纯 TypeScript,浏览器 / Node / Bun / Deno 通用。
|
|
5
|
+
*
|
|
6
|
+
* 路由结构:
|
|
7
|
+
* /api/auth/* — 鉴权
|
|
8
|
+
* /api/query/:table — POST Body 查询(前端推荐)
|
|
9
|
+
* /api/delete/:table — POST Body 条件删除(前端推荐)
|
|
10
|
+
* /api/data/:table — CRUD(POST 创建 / PUT upsert)
|
|
11
|
+
* /api/data/:table/:id — 按主键 GET / DELETE
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* const rb = new RestBase("http://localhost:3333");
|
|
16
|
+
* await rb.auth.login("admin", "admin");
|
|
17
|
+
* const data = await rb.table("products").query().where(gt("price", 100)).data();
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/* ══════════════════════════════════════════════════════════════
|
|
22
|
+
统一响应类型
|
|
23
|
+
══════════════════════════════════════════════════════════════ */
|
|
24
|
+
|
|
25
|
+
export interface ApiResponse<T = unknown> {
|
|
26
|
+
code: string;
|
|
27
|
+
message?: string;
|
|
28
|
+
data: T;
|
|
29
|
+
pageNo?: number;
|
|
30
|
+
pageSize?: number;
|
|
31
|
+
total?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 表元数据 */
|
|
35
|
+
export interface TableMeta {
|
|
36
|
+
name: string;
|
|
37
|
+
pk: string | null;
|
|
38
|
+
hasOwner: boolean;
|
|
39
|
+
columns: { name: string; type: string; isNumeric: boolean }[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* ══════════════════════════════════════════════════════════════
|
|
43
|
+
WHERE 条件 DSL(与服务端 Body 格式一一对应)
|
|
44
|
+
══════════════════════════════════════════════════════════════ */
|
|
45
|
+
|
|
46
|
+
/** 条件节点(内部表示,直接序列化为服务端 JSON) */
|
|
47
|
+
export type Condition =
|
|
48
|
+
| { type: "tuple"; field: string; op: string; value: unknown }
|
|
49
|
+
| { type: "group"; logic: "and" | "or"; children: Condition[] };
|
|
50
|
+
|
|
51
|
+
/* ── 比较运算符 ── */
|
|
52
|
+
|
|
53
|
+
export const eq = (f: string, v: unknown): Condition =>
|
|
54
|
+
({type: "tuple", field: f, op: "eq", value: v});
|
|
55
|
+
|
|
56
|
+
export const ne = (f: string, v: unknown): Condition =>
|
|
57
|
+
({type: "tuple", field: f, op: "ne", value: v});
|
|
58
|
+
|
|
59
|
+
export const gt = (f: string, v: unknown): Condition =>
|
|
60
|
+
({type: "tuple", field: f, op: "gt", value: v});
|
|
61
|
+
|
|
62
|
+
export const ge = (f: string, v: unknown): Condition =>
|
|
63
|
+
({type: "tuple", field: f, op: "ge", value: v});
|
|
64
|
+
|
|
65
|
+
export const lt = (f: string, v: unknown): Condition =>
|
|
66
|
+
({type: "tuple", field: f, op: "lt", value: v});
|
|
67
|
+
|
|
68
|
+
export const le = (f: string, v: unknown): Condition =>
|
|
69
|
+
({type: "tuple", field: f, op: "le", value: v});
|
|
70
|
+
|
|
71
|
+
/* ── NULL 判断 ── */
|
|
72
|
+
|
|
73
|
+
export const isNull = (f: string): Condition =>
|
|
74
|
+
({type: "tuple", field: f, op: "is", value: null});
|
|
75
|
+
|
|
76
|
+
export const isNotNull = (f: string): Condition =>
|
|
77
|
+
({type: "tuple", field: f, op: "nis", value: null});
|
|
78
|
+
|
|
79
|
+
/* ── LIKE ── */
|
|
80
|
+
|
|
81
|
+
export const like = (f: string, pattern: string): Condition =>
|
|
82
|
+
({type: "tuple", field: f, op: "like", value: pattern});
|
|
83
|
+
|
|
84
|
+
export const nlike = (f: string, pattern: string): Condition =>
|
|
85
|
+
({type: "tuple", field: f, op: "nlike", value: pattern});
|
|
86
|
+
|
|
87
|
+
/* ── IN / NOT IN / BETWEEN ── */
|
|
88
|
+
|
|
89
|
+
export const isIn = (f: string, values: unknown[]): Condition =>
|
|
90
|
+
({type: "tuple", field: f, op: "in", value: values});
|
|
91
|
+
|
|
92
|
+
export const notIn = (f: string, values: unknown[]): Condition =>
|
|
93
|
+
({type: "tuple", field: f, op: "nin", value: values});
|
|
94
|
+
|
|
95
|
+
export const between = (f: string, lo: unknown, hi: unknown): Condition =>
|
|
96
|
+
({type: "tuple", field: f, op: "between", value: [lo, hi]});
|
|
97
|
+
|
|
98
|
+
/* ── 逻辑组合 ── */
|
|
99
|
+
|
|
100
|
+
export const and = (...conds: Condition[]): Condition =>
|
|
101
|
+
({type: "group", logic: "and", children: conds});
|
|
102
|
+
|
|
103
|
+
export const or = (...conds: Condition[]): Condition =>
|
|
104
|
+
({type: "group", logic: "or", children: conds});
|
|
105
|
+
|
|
106
|
+
/* ── 序列化为服务端 where 数组 ── */
|
|
107
|
+
|
|
108
|
+
function condToBody(c: Condition): unknown {
|
|
109
|
+
if (c.type === "tuple") {
|
|
110
|
+
return [c.field, c.op, c.value];
|
|
111
|
+
}
|
|
112
|
+
return {op: c.logic, cond: c.children.map(condToBody)};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function conditionsToWhere(conds: Condition[]): unknown {
|
|
116
|
+
if (conds.length === 0) return undefined;
|
|
117
|
+
if (conds.length === 1) return condToBody(conds[0]!);
|
|
118
|
+
return conds.map(condToBody);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* ══════════════════════════════════════════════════════════════
|
|
122
|
+
SELECT 项构建器
|
|
123
|
+
══════════════════════════════════════════════════════════════ */
|
|
124
|
+
|
|
125
|
+
export interface SelectItem {
|
|
126
|
+
field: string;
|
|
127
|
+
alias?: string;
|
|
128
|
+
fn?: string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* ── 类型安全 SELECT:编译期推导查询结果类型 ── */
|
|
132
|
+
|
|
133
|
+
/** 展平交叉类型为扁平对象 */
|
|
134
|
+
type Prettify<T> = { [K in keyof T]: T[K] } & {};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 映射单个 select 参数到输出类型
|
|
138
|
+
*
|
|
139
|
+
* 优先级(从高到低):
|
|
140
|
+
* 1. 字符串且为 keyof T → Pick<T, field>
|
|
141
|
+
* 2. "func:field:alias" 模板 → { alias: number }
|
|
142
|
+
* 3. "field:alias" (field ∈ keyof T) → { alias: T[field] }(字段重命名)
|
|
143
|
+
* 4. "func:field" (func ∉ keyof T) → { "func:field": number }(聚合,key = 原字符串)
|
|
144
|
+
* 5. 对象且含 fn + alias(聚合) → { alias: number }
|
|
145
|
+
* 6. 对象含 field + alias(重命名) → { alias: T[field] }
|
|
146
|
+
* 7. 对象仅含 field → Pick<T, field>
|
|
147
|
+
*/
|
|
148
|
+
type MapSelectArg<T, Item> =
|
|
149
|
+
Item extends keyof T & string ? { [K in Item]: T[Item] }
|
|
150
|
+
: Item extends `${string}:${string}:${infer A}` ? { [K in A]: number }
|
|
151
|
+
: Item extends `${infer F}:${infer A}`
|
|
152
|
+
? F extends keyof T ? { [K in A]: T[F] } : { [K in Item & string]: number }
|
|
153
|
+
: Item extends { fn: string; alias: infer A extends string } ? { [K in A]: number }
|
|
154
|
+
: Item extends { field: infer F; alias: infer A extends string }
|
|
155
|
+
? F extends keyof T ? { [K in A]: T[F] } : {}
|
|
156
|
+
: Item extends { field: infer F }
|
|
157
|
+
? F extends keyof T & string ? { [K in F]: T[F] } : {}
|
|
158
|
+
: {};
|
|
159
|
+
|
|
160
|
+
/** 递归合并 select 参数元组为单一对象类型 */
|
|
161
|
+
type MergeSelectArgs<T, Items extends readonly unknown[]> =
|
|
162
|
+
Items extends readonly [infer H, ...infer R]
|
|
163
|
+
? MapSelectArg<T, H> & MergeSelectArgs<T, R>
|
|
164
|
+
: {};
|
|
165
|
+
|
|
166
|
+
/** 普通字段或带别名(保留字面量类型) */
|
|
167
|
+
export function sel<F extends string>(field: F): { field: F; alias: undefined; fn: undefined };
|
|
168
|
+
export function sel<F extends string, A extends string>(field: F, alias: A): { field: F; alias: A; fn: undefined };
|
|
169
|
+
export function sel(field: string, alias?: string): any {
|
|
170
|
+
return {field, alias, fn: undefined};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** 聚合函数(保留 alias 字面量类型;无 alias 时默认 "fn:field") */
|
|
174
|
+
export function agg<Fn extends string, F extends string>(fn: Fn, field: F): { field: F; fn: Fn; alias: `${Fn}:${F}` };
|
|
175
|
+
export function agg<A extends string>(fn: string, field: string, alias: A): { field: string; fn: string; alias: A };
|
|
176
|
+
export function agg(fn: string, field: string, alias?: string): any {
|
|
177
|
+
return {field, alias: alias ?? `${fn}:${field}`, fn};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function selectToBody(items: (string | SelectItem)[]): unknown[] {
|
|
181
|
+
return items.map((item) => {
|
|
182
|
+
if (typeof item === "string") return item;
|
|
183
|
+
if (item.fn && item.alias) return {field: item.field, func: item.fn, alias: item.alias};
|
|
184
|
+
if (item.fn) return {field: item.field, func: item.fn};
|
|
185
|
+
if (item.alias) return {field: item.field, alias: item.alias};
|
|
186
|
+
return item.field;
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/* ══════════════════════════════════════════════════════════════
|
|
191
|
+
ORDER 构建器
|
|
192
|
+
══════════════════════════════════════════════════════════════ */
|
|
193
|
+
|
|
194
|
+
interface OrderEntry {
|
|
195
|
+
field: string;
|
|
196
|
+
dir: "asc" | "desc"
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function orderToBody(entries: OrderEntry[]): unknown[] {
|
|
200
|
+
return entries.map((e) => ({field: e.field, dir: e.dir}));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/* ══════════════════════════════════════════════════════════════
|
|
204
|
+
QueryBuilder — 链式查询(POST /api/query/:table)
|
|
205
|
+
══════════════════════════════════════════════════════════════ */
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* QueryBuilder — 链式查询,类型安全的 select
|
|
209
|
+
*
|
|
210
|
+
* 泛型参数:
|
|
211
|
+
* T = 原始表类型(始终不变,用于约束 select 参数)
|
|
212
|
+
* S = 投影后的输出类型(由 select() 推导,默认等于 T)
|
|
213
|
+
*
|
|
214
|
+
* @example
|
|
215
|
+
* ```ts
|
|
216
|
+
* interface Product { id: number; name: string; price: number; stock: number }
|
|
217
|
+
* const q = rb.table<Product>("products").query();
|
|
218
|
+
*
|
|
219
|
+
* // S = { name: string; price: number }
|
|
220
|
+
* const a = await q.select("name", "price").data();
|
|
221
|
+
*
|
|
222
|
+
* // S = { unitPrice: number; name: string }
|
|
223
|
+
* const b = await q.select(sel("price", "unitPrice"), "name").data();
|
|
224
|
+
*
|
|
225
|
+
* // S = { category: string; total: number; avgPrice: number }
|
|
226
|
+
* const c = await q.select("category", agg("count","id","total"), agg("avg","price","avgPrice"))
|
|
227
|
+
* .groupBy("category").data();
|
|
228
|
+
* ```
|
|
229
|
+
*/
|
|
230
|
+
export class QueryBuilder<T = Record<string, unknown>, S = T> {
|
|
231
|
+
private _conditions: Condition[] = [];
|
|
232
|
+
private _select: (string | SelectItem)[] = [];
|
|
233
|
+
private _order: OrderEntry[] = [];
|
|
234
|
+
private _group: string[] = [];
|
|
235
|
+
private _pageNo?: number;
|
|
236
|
+
private _pageSize?: number;
|
|
237
|
+
private _execFn: (body: unknown) => Promise<ApiResponse<any[]>>;
|
|
238
|
+
|
|
239
|
+
constructor(execFn: (body: unknown) => Promise<ApiResponse<any[]>>) {
|
|
240
|
+
this._execFn = execFn;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** 添加 WHERE 条件(多次调用为 AND 关系) */
|
|
244
|
+
where(...conditions: Condition[]): QueryBuilder<T, S> {
|
|
245
|
+
this._conditions.push(...conditions);
|
|
246
|
+
return this;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* 指定查询字段 — 自动推导返回类型
|
|
251
|
+
*
|
|
252
|
+
* - `"field"` → 保留原类型 `{ field: T[field] }`
|
|
253
|
+
* - `sel("price", "unitPrice")` → 重命名 `{ unitPrice: T["price"] }`
|
|
254
|
+
* - `agg("count", "id", "total")` → 聚合 `{ total: number }`
|
|
255
|
+
* - `"func:field:alias"` → 模板推导 `{ alias: number }`
|
|
256
|
+
*/
|
|
257
|
+
select<const Items extends readonly ((keyof T & string) | (string & {}) | SelectItem)[]>(
|
|
258
|
+
...items: Items
|
|
259
|
+
): QueryBuilder<T, Prettify<MergeSelectArgs<T, Items>>> {
|
|
260
|
+
this._select.push(...(items as any));
|
|
261
|
+
return this as any;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** 升序排序 */
|
|
265
|
+
orderAsc(...fields: string[]): QueryBuilder<T, S> {
|
|
266
|
+
for (const f of fields) this._order.push({field: f, dir: "asc"});
|
|
267
|
+
return this;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** 降序排序 */
|
|
271
|
+
orderDesc(...fields: string[]): QueryBuilder<T, S> {
|
|
272
|
+
for (const f of fields) this._order.push({field: f, dir: "desc"});
|
|
273
|
+
return this;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** 分组 */
|
|
277
|
+
groupBy(...fields: string[]): QueryBuilder<T, S> {
|
|
278
|
+
this._group.push(...fields);
|
|
279
|
+
return this;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** 分页 */
|
|
283
|
+
page(pageNo: number, pageSize: number): QueryBuilder<T, S> {
|
|
284
|
+
this._pageNo = pageNo;
|
|
285
|
+
this._pageSize = pageSize;
|
|
286
|
+
return this;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** 构建请求 Body */
|
|
290
|
+
build(): Record<string, unknown> {
|
|
291
|
+
const body: Record<string, unknown> = {};
|
|
292
|
+
const w = conditionsToWhere(this._conditions);
|
|
293
|
+
if (w !== undefined) body.where = w;
|
|
294
|
+
if (this._select.length > 0) body.select = selectToBody(this._select);
|
|
295
|
+
if (this._order.length > 0) body.order = orderToBody(this._order);
|
|
296
|
+
if (this._group.length > 0) body.group = this._group;
|
|
297
|
+
if (this._pageNo !== undefined) body.pageNo = this._pageNo;
|
|
298
|
+
if (this._pageSize !== undefined) body.pageSize = this._pageSize;
|
|
299
|
+
return body;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** 执行查询,返回完整响应(类型随 select 变化) */
|
|
303
|
+
async exec(): Promise<ApiResponse<S[]>> {
|
|
304
|
+
return this._execFn(this.build()) as any;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** 仅返回数据数组 */
|
|
308
|
+
async data(): Promise<S[]> {
|
|
309
|
+
const res = await this.exec();
|
|
310
|
+
return res.data ?? [];
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** 返回第一条 */
|
|
314
|
+
async first(): Promise<S | null> {
|
|
315
|
+
const res = await this.page(1, 1).exec();
|
|
316
|
+
return res.data?.[0] ?? null;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/* ══════════════════════════════════════════════════════════════
|
|
321
|
+
DeleteBuilder — 链式条件删除(POST /api/delete/:table)
|
|
322
|
+
══════════════════════════════════════════════════════════════ */
|
|
323
|
+
|
|
324
|
+
export class DeleteBuilder {
|
|
325
|
+
private _conditions: Condition[] = [];
|
|
326
|
+
private _execFn: (body: unknown) => Promise<ApiResponse<{ deleted: unknown[] }>>;
|
|
327
|
+
|
|
328
|
+
constructor(execFn: (body: unknown) => Promise<ApiResponse<{ deleted: unknown[] }>>) {
|
|
329
|
+
this._execFn = execFn;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** 添加 WHERE 条件 */
|
|
333
|
+
where(...conditions: Condition[]): this {
|
|
334
|
+
this._conditions.push(...conditions);
|
|
335
|
+
return this;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** 执行删除,返回被删除记录的主键列表 */
|
|
339
|
+
async exec(): Promise<ApiResponse<{ deleted: unknown[] }>> {
|
|
340
|
+
const body = conditionsToWhere(this._conditions) ?? [];
|
|
341
|
+
return this._execFn(body);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/* ══════════════════════════════════════════════════════════════
|
|
346
|
+
TableClient — 单表操作
|
|
347
|
+
══════════════════════════════════════════════════════════════ */
|
|
348
|
+
|
|
349
|
+
export class TableClient<T = Record<string, unknown>> {
|
|
350
|
+
private _http: HttpClient;
|
|
351
|
+
private _name: string;
|
|
352
|
+
|
|
353
|
+
constructor(http: HttpClient, name: string) {
|
|
354
|
+
this._http = http;
|
|
355
|
+
this._name = name;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** 创建链式查询(POST /api/query/:table) */
|
|
359
|
+
query(): QueryBuilder<T> {
|
|
360
|
+
return new QueryBuilder<T>((body) =>
|
|
361
|
+
this._http.post<T[]>(`/api/query/${this._name}`, body),
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** 按主键获取单条(GET /api/data/:table/:id) */
|
|
366
|
+
async getByPk(id: string | number): Promise<ApiResponse<T | null>> {
|
|
367
|
+
return this._http.get<T | null>(`/api/data/${this._name}/${id}`);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** 创建记录(POST /api/data/:table),返回 { created: [主键值...] } */
|
|
371
|
+
async create(data: Partial<T> | Partial<T>[]): Promise<ApiResponse<{ created: unknown[] }>> {
|
|
372
|
+
return this._http.post<{ created: unknown[] }>(`/api/data/${this._name}`, data);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** 不存在创建、存在增量更新(PUT /api/data/:table),返回 { created: [...], updated: [...] } */
|
|
376
|
+
async put(data: Partial<T> | Partial<T>[]): Promise<ApiResponse<{ created: unknown[]; updated: unknown[] }>> {
|
|
377
|
+
return this._http.put<{ created: unknown[]; updated: unknown[] }>(`/api/data/${this._name}`, data);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** 按主键删除(DELETE /api/data/:table/:id),返回 { deleted: [主键值] } */
|
|
381
|
+
async deleteByPk(id: string | number): Promise<ApiResponse<{ deleted: unknown[] }>> {
|
|
382
|
+
return this._http.delete<{ deleted: unknown[] }>(`/api/data/${this._name}/${id}`);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** 创建链式条件删除(POST /api/delete/:table) */
|
|
386
|
+
deleteWhere(): DeleteBuilder {
|
|
387
|
+
return new DeleteBuilder((body) =>
|
|
388
|
+
this._http.post<{ deleted: unknown[] }>(`/api/delete/${this._name}`, body),
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/* ══════════════════════════════════════════════════════════════
|
|
394
|
+
AuthClient — 鉴权
|
|
395
|
+
══════════════════════════════════════════════════════════════ */
|
|
396
|
+
|
|
397
|
+
export class AuthClient {
|
|
398
|
+
private _http: HttpClient;
|
|
399
|
+
|
|
400
|
+
constructor(http: HttpClient) {
|
|
401
|
+
this._http = http;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** 登录,成功后自动设置 JWT token */
|
|
405
|
+
async login(username: string, password: string): Promise<ApiResponse<string>> {
|
|
406
|
+
const res = await this._http.post<string>("/api/auth/login", {username, password});
|
|
407
|
+
if (res.code === "OK" && res.data) this._http.setToken(res.data);
|
|
408
|
+
return res;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** 注册,成功后自动设置 JWT token */
|
|
412
|
+
async register(username: string, password: string): Promise<ApiResponse<string>> {
|
|
413
|
+
const res = await this._http.post<string>("/api/auth/register", {username, password});
|
|
414
|
+
if (res.code === "OK" && res.data) this._http.setToken(res.data);
|
|
415
|
+
return res;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** 获取当前用户资料 */
|
|
419
|
+
async getProfile<P = Record<string, unknown>>(): Promise<ApiResponse<P>> {
|
|
420
|
+
return this._http.get<P>("/api/auth/profile");
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** 更新当前用户资料 */
|
|
424
|
+
async updateProfile(data: Record<string, unknown>): Promise<ApiResponse<null>> {
|
|
425
|
+
return this._http.post<null>("/api/auth/profile", data);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** 手动设置 token(如从 localStorage 恢复) */
|
|
429
|
+
setToken(token: string): void {
|
|
430
|
+
this._http.setToken(token);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** 获取当前 token */
|
|
434
|
+
getToken(): string | null {
|
|
435
|
+
return this._http.getToken();
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/** 清除 token(登出) */
|
|
439
|
+
logout(): void {
|
|
440
|
+
this._http.setToken(null);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** 切换为 Basic Auth */
|
|
444
|
+
useBasicAuth(username: string, password: string): void {
|
|
445
|
+
this._http.setBasicAuth(username, password);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/* ══════════════════════════════════════════════════════════════
|
|
450
|
+
HttpClient — 底层 HTTP(内部使用)
|
|
451
|
+
══════════════════════════════════════════════════════════════ */
|
|
452
|
+
|
|
453
|
+
class HttpClient {
|
|
454
|
+
private _baseUrl: string;
|
|
455
|
+
private _token: string | null = null;
|
|
456
|
+
private _basicAuth: string | null = null;
|
|
457
|
+
private _requestId?: string;
|
|
458
|
+
private _headers: Record<string, string> = {};
|
|
459
|
+
|
|
460
|
+
constructor(baseUrl: string) {
|
|
461
|
+
this._baseUrl = baseUrl.replace(/\/+$/, "");
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
setToken(token: string | null): void {
|
|
465
|
+
this._token = token;
|
|
466
|
+
this._basicAuth = null;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
getToken(): string | null {
|
|
470
|
+
return this._token;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
setBasicAuth(username: string, password: string): void {
|
|
474
|
+
this._basicAuth = btoa(`${username}:${password}`);
|
|
475
|
+
this._token = null;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
setRequestId(id: string | undefined): void {
|
|
479
|
+
this._requestId = id;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
setHeader(key: string, value: string): void {
|
|
483
|
+
this._headers[key] = value;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
get<T>(path: string, params?: Record<string, string>): Promise<ApiResponse<T>> {
|
|
487
|
+
return this._fetch<T>("GET", path, params);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
post<T>(path: string, body?: unknown): Promise<ApiResponse<T>> {
|
|
491
|
+
return this._fetch<T>("POST", path, undefined, body);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
put<T>(path: string, body?: unknown): Promise<ApiResponse<T>> {
|
|
495
|
+
return this._fetch<T>("PUT", path, undefined, body);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
delete<T>(path: string, params?: Record<string, string>): Promise<ApiResponse<T>> {
|
|
499
|
+
return this._fetch<T>("DELETE", path, params);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
private _buildHeaders(): Record<string, string> {
|
|
503
|
+
const h: Record<string, string> = {
|
|
504
|
+
"Content-Type": "application/json",
|
|
505
|
+
...this._headers,
|
|
506
|
+
};
|
|
507
|
+
if (this._token) h["Authorization"] = `Bearer ${this._token}`;
|
|
508
|
+
else if (this._basicAuth) h["Authorization"] = `Basic ${this._basicAuth}`;
|
|
509
|
+
if (this._requestId) h["X-Request-Id"] = this._requestId;
|
|
510
|
+
return h;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
private async _fetch<T>(method: string, path: string, params?: Record<string, string>, body?: unknown): Promise<ApiResponse<T>> {
|
|
514
|
+
let url = `${this._baseUrl}${path}`;
|
|
515
|
+
if (params && Object.keys(params).length > 0) {
|
|
516
|
+
url += `?${new URLSearchParams(params).toString()}`;
|
|
517
|
+
}
|
|
518
|
+
const init: RequestInit = {method, headers: this._buildHeaders()};
|
|
519
|
+
if (body !== undefined) init.body = JSON.stringify(body);
|
|
520
|
+
const res = await fetch(url, init);
|
|
521
|
+
return (await res.json()) as ApiResponse<T>;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/* ══════════════════════════════════════════════════════════════
|
|
526
|
+
RestBase — 主入口
|
|
527
|
+
══════════════════════════════════════════════════════════════ */
|
|
528
|
+
|
|
529
|
+
export class RestBase {
|
|
530
|
+
readonly auth: AuthClient;
|
|
531
|
+
private _http: HttpClient;
|
|
532
|
+
|
|
533
|
+
constructor(endpoint: string) {
|
|
534
|
+
this._http = new HttpClient(endpoint);
|
|
535
|
+
this.auth = new AuthClient(this._http);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/** 获取表操作客户端 */
|
|
539
|
+
table<T = Record<string, unknown>>(name: string): TableClient<T> {
|
|
540
|
+
return new TableClient<T>(this._http, name);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/** 健康检查 */
|
|
544
|
+
async health(): Promise<ApiResponse<{ status: string }>> {
|
|
545
|
+
return this._http.get<{ status: string }>("/api/health");
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/** 获取所有表元数据(不含 users 表) */
|
|
549
|
+
async tables(): Promise<ApiResponse<TableMeta[]>> {
|
|
550
|
+
return this._http.get<TableMeta[]>("/api/meta/tables");
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/** 获取指定表的元数据 */
|
|
554
|
+
async tableMeta(name: string): Promise<ApiResponse<TableMeta | null>> {
|
|
555
|
+
return this._http.get<TableMeta | null>(`/api/meta/tables/${name}`);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/** 运行时同步数据库表结构(新建表后调用) */
|
|
559
|
+
async syncMeta(): Promise<ApiResponse<TableMeta[]>> {
|
|
560
|
+
return this._http.get<TableMeta[]>("/api/meta/sync");
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/** 设置自定义请求头 */
|
|
564
|
+
setHeader(key: string, value: string): this {
|
|
565
|
+
this._http.setHeader(key, value);
|
|
566
|
+
return this;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/** 设置请求追踪 ID */
|
|
570
|
+
setRequestId(id: string): this {
|
|
571
|
+
this._http.setRequestId(id);
|
|
572
|
+
return this;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
export default RestBase;
|