@dtdyq/restbase 2.0.0 → 2.1.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 +17 -23
- package/bin/restbase.ts +298 -192
- package/client/README.md +35 -13
- package/client/package.json +1 -1
- package/client/restbase-client.ts +39 -10
- package/package.json +3 -1
- package/src/logger.ts +4 -1
- package/src/server.ts +3 -1
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ bun install -g @dtdyq/restbase
|
|
|
24
24
|
安装后在任意目录使用 `restbase` 命令。所有配置通过 `.env` 文件读取:
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
|
-
#
|
|
27
|
+
# 交互式配置 .env(已存在则用当前值作为默认)
|
|
28
28
|
restbase env
|
|
29
29
|
|
|
30
30
|
# 前台启动(读取当前目录 .env)
|
|
@@ -36,11 +36,11 @@ restbase start
|
|
|
36
36
|
# 查看运行中的实例(含健康状态)
|
|
37
37
|
restbase status
|
|
38
38
|
|
|
39
|
-
#
|
|
40
|
-
restbase log <pid>
|
|
39
|
+
# 查看某个实例的实时日志(支持 PID 或 SVR_NAME)
|
|
40
|
+
restbase log <pid|name>
|
|
41
41
|
|
|
42
|
-
#
|
|
43
|
-
restbase stop <pid>
|
|
42
|
+
# 停止(支持 PID、SVR_NAME 或 all)
|
|
43
|
+
restbase stop <pid|name> # 停止指定实例
|
|
44
44
|
restbase stop all # 停止所有实例
|
|
45
45
|
```
|
|
46
46
|
|
|
@@ -60,9 +60,9 @@ bun run src/server.ts
|
|
|
60
60
|
### 验证
|
|
61
61
|
|
|
62
62
|
```bash
|
|
63
|
-
# 健康检查(返回实例状态 +
|
|
63
|
+
# 健康检查(返回实例状态 + 资源占用 + cwd + logFile)
|
|
64
64
|
curl http://localhost:3333/api/health
|
|
65
|
-
# → { "code": "OK", "data": { "status": "healthy", "port": 3333, "pid": 12345, "uptime": 60, "memory": {...}, "cpu": {...} } }
|
|
65
|
+
# → { "code": "OK", "data": { "status": "healthy", "port": 3333, "pid": 12345, "cwd": "/path/to/dir", "uptime": 60, "memory": {...}, "cpu": {...} } }
|
|
66
66
|
|
|
67
67
|
# Basic Auth 查询
|
|
68
68
|
curl -u admin:admin http://localhost:3333/api/data/products
|
|
@@ -82,6 +82,10 @@ import RestBase, { gt } from "@dtdyq/restbase-client";
|
|
|
82
82
|
const rb = new RestBase(); // 同源部署不传参
|
|
83
83
|
await rb.auth.login("admin", "admin");
|
|
84
84
|
const data = await rb.table("products").query().where(gt("price", 100)).data();
|
|
85
|
+
|
|
86
|
+
// 多节点负载均衡(所有节点连同一 DB)
|
|
87
|
+
const rb2 = new RestBase(["http://node1:3333", "http://node2:3333"]);
|
|
88
|
+
await rb2.auth.login("admin", "admin"); // 一次登录,token 共享
|
|
85
89
|
```
|
|
86
90
|
|
|
87
91
|
详见 [client/README.md](client/README.md)。
|
|
@@ -139,28 +143,18 @@ restbase <command> [arguments]
|
|
|
139
143
|
Commands:
|
|
140
144
|
run 前台启动服务(读取当前目录 .env)
|
|
141
145
|
start 后台启动(daemon 模式)
|
|
142
|
-
stop <pid|all>
|
|
143
|
-
status
|
|
144
|
-
log <pid>
|
|
145
|
-
env
|
|
146
|
+
stop <pid|name|all> 停止后台实例(支持 PID、SVR_NAME 或 all)
|
|
147
|
+
status 查看运行中的实例(含健康检查)
|
|
148
|
+
log <pid|name> 实时查看实例日志(支持 PID 或 SVR_NAME)
|
|
149
|
+
env 交互式 .env 配置(创建或重新配置)
|
|
146
150
|
version 显示版本号
|
|
147
151
|
help 显示帮助
|
|
148
152
|
```
|
|
149
153
|
|
|
150
154
|
所有配置通过 `.env` 文件管理,可在 `.env` 中设置 `SVR_NAME` 为实例命名。
|
|
151
155
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
```
|
|
155
|
-
~/.restbase/
|
|
156
|
-
├── instances/ # 实例元数据(name/port/pid/logPath)
|
|
157
|
-
│ ├── 12345.json
|
|
158
|
-
│ └── 23456.json
|
|
159
|
-
└── logs/ # daemon 默认日志目录
|
|
160
|
-
├── 3333.log
|
|
161
|
-
├── 3333.out
|
|
162
|
-
└── 8080.log
|
|
163
|
-
```
|
|
156
|
+
实例管理完全无状态:通过 `ps` 自动发现运行中的实例,通过 `/api/health` 获取实例详情。
|
|
157
|
+
daemon 模式默认日志存放在 `~/.restbase/logs/`。
|
|
164
158
|
|
|
165
159
|
## 文件结构
|
|
166
160
|
|
package/bin/restbase.ts
CHANGED
|
@@ -1,91 +1,73 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/**
|
|
3
|
-
* bin/restbase.ts — CLI entry point
|
|
3
|
+
* bin/restbase.ts — CLI entry point (stateless management)
|
|
4
4
|
*
|
|
5
5
|
* Commands:
|
|
6
6
|
* restbase run Start server in foreground (reads .env)
|
|
7
7
|
* restbase start Start server in background (daemon)
|
|
8
|
-
* restbase stop <pid|all>
|
|
9
|
-
* restbase status
|
|
10
|
-
* restbase log <pid>
|
|
8
|
+
* restbase stop <pid|name|all> Stop instance(s) by PID, name or all
|
|
9
|
+
* restbase status Show running instances (table + health check)
|
|
10
|
+
* restbase log <pid|name> Tail log of a background instance
|
|
11
11
|
* restbase env Generate .env template in current directory
|
|
12
12
|
* restbase version Show version
|
|
13
13
|
* restbase help Show help
|
|
14
14
|
*
|
|
15
15
|
* All configuration is read from .env in the current working directory.
|
|
16
|
-
*
|
|
16
|
+
*
|
|
17
|
+
* Instance discovery is stateless:
|
|
18
|
+
* - `restbase start` spawns the child with a `--port=N` marker arg
|
|
19
|
+
* - Management commands use `ps` to find processes matching `restbase.*--port=`
|
|
20
|
+
* - Detailed info (name, logFile, cwd, uptime…) is fetched via /api/health
|
|
17
21
|
*/
|
|
18
22
|
|
|
19
|
-
import {spawn} from "child_process";
|
|
23
|
+
import {spawn, execSync} from "child_process";
|
|
20
24
|
import {
|
|
21
25
|
existsSync,
|
|
22
26
|
mkdirSync,
|
|
23
|
-
readFileSync,
|
|
24
|
-
unlinkSync,
|
|
25
|
-
readdirSync,
|
|
26
27
|
openSync,
|
|
27
28
|
writeFileSync,
|
|
28
29
|
} from "fs";
|
|
29
30
|
import {resolve, join} from "path";
|
|
31
|
+
import {readFileSync} from "fs";
|
|
30
32
|
import {homedir} from "os";
|
|
33
|
+
import * as p from "@clack/prompts";
|
|
31
34
|
|
|
32
35
|
/* ═══════════ Global paths ═══════════ */
|
|
33
36
|
|
|
34
37
|
const RESTBASE_HOME = resolve(homedir(), ".restbase");
|
|
35
|
-
const INSTANCES_DIR = join(RESTBASE_HOME, "instances");
|
|
36
38
|
const LOGS_DIR = join(RESTBASE_HOME, "logs");
|
|
37
39
|
|
|
38
|
-
function
|
|
39
|
-
mkdirSync(INSTANCES_DIR, {recursive: true});
|
|
40
|
+
function ensureLogDir() {
|
|
40
41
|
mkdirSync(LOGS_DIR, {recursive: true});
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
/* ═══════════
|
|
44
|
+
/* ═══════════ Process discovery via ps ═══════════ */
|
|
44
45
|
|
|
45
|
-
interface
|
|
46
|
+
interface PsInstance {
|
|
46
47
|
pid: number;
|
|
47
|
-
name: string;
|
|
48
48
|
port: number;
|
|
49
|
-
logPath: string;
|
|
50
|
-
cwd: string;
|
|
51
|
-
startedAt: string;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function instanceFile(pid: number) {
|
|
55
|
-
return join(INSTANCES_DIR, `${pid}.json`);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function saveInstance(meta: InstanceMeta) {
|
|
59
|
-
writeFileSync(instanceFile(meta.pid), JSON.stringify(meta, null, 2));
|
|
60
49
|
}
|
|
61
50
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Scan running processes for `restbase.*--port=N` and extract PID + port.
|
|
53
|
+
* Works on macOS and Linux.
|
|
54
|
+
*/
|
|
55
|
+
function discoverInstances(): PsInstance[] {
|
|
65
56
|
try {
|
|
66
|
-
|
|
57
|
+
const out = execSync("ps -eo pid,args", {encoding: "utf-8"});
|
|
58
|
+
const results: PsInstance[] = [];
|
|
59
|
+
for (const line of out.split("\n")) {
|
|
60
|
+
const match = line.match(/^\s*(\d+)\s+.*restbase.*--port=(\d+)/);
|
|
61
|
+
if (match) {
|
|
62
|
+
results.push({pid: Number(match[1]), port: Number(match[2])});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return results;
|
|
67
66
|
} catch {
|
|
68
|
-
return
|
|
67
|
+
return [];
|
|
69
68
|
}
|
|
70
69
|
}
|
|
71
70
|
|
|
72
|
-
function loadAllInstances(): InstanceMeta[] {
|
|
73
|
-
ensureDirs();
|
|
74
|
-
const files = readdirSync(INSTANCES_DIR).filter((f) => f.endsWith(".json"));
|
|
75
|
-
const result: InstanceMeta[] = [];
|
|
76
|
-
for (const f of files) {
|
|
77
|
-
try {
|
|
78
|
-
result.push(JSON.parse(readFileSync(join(INSTANCES_DIR, f), "utf-8")));
|
|
79
|
-
} catch { /* corrupted, skip */ }
|
|
80
|
-
}
|
|
81
|
-
return result;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function removeInstance(pid: number) {
|
|
85
|
-
const f = instanceFile(pid);
|
|
86
|
-
if (existsSync(f)) unlinkSync(f);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
71
|
/* ═══════════ Process helpers ═══════════ */
|
|
90
72
|
|
|
91
73
|
function isAlive(pid: number): boolean {
|
|
@@ -102,6 +84,7 @@ interface HealthInfo {
|
|
|
102
84
|
name?: string;
|
|
103
85
|
port?: number;
|
|
104
86
|
pid?: number;
|
|
87
|
+
cwd?: string;
|
|
105
88
|
logFile?: string;
|
|
106
89
|
startedAt?: string;
|
|
107
90
|
uptime?: number;
|
|
@@ -141,31 +124,25 @@ async function cmdRun() {
|
|
|
141
124
|
|
|
142
125
|
/** start — daemon mode */
|
|
143
126
|
async function cmdStart() {
|
|
144
|
-
|
|
127
|
+
ensureLogDir();
|
|
145
128
|
|
|
146
129
|
const port = Number(process.env.SVR_PORT) || 3333;
|
|
147
|
-
const name = process.env.SVR_NAME || "";
|
|
148
130
|
|
|
149
131
|
// Reject if this port is already running
|
|
150
|
-
for (const inst of
|
|
151
|
-
if (inst.port === port
|
|
132
|
+
for (const inst of discoverInstances()) {
|
|
133
|
+
if (inst.port === port) {
|
|
152
134
|
console.log(`RestBase already running on port ${port} (PID: ${inst.pid})`);
|
|
153
135
|
return;
|
|
154
136
|
}
|
|
155
137
|
}
|
|
156
138
|
|
|
157
|
-
//
|
|
158
|
-
const logPath = process.env.LOG_FILE
|
|
159
|
-
? resolve(process.cwd(), process.env.LOG_FILE)
|
|
160
|
-
: join(LOGS_DIR, `${port}.log`);
|
|
161
|
-
|
|
162
|
-
// Build child env: silence console, ensure LOG_FILE
|
|
139
|
+
// Build child env: silence console, ensure LOG_FILE for daemon
|
|
163
140
|
const env: Record<string, string> = {
|
|
164
141
|
...(process.env as Record<string, string>),
|
|
165
142
|
LOG_CONSOLE: "false",
|
|
166
143
|
};
|
|
167
144
|
if (!process.env.LOG_FILE) {
|
|
168
|
-
env.LOG_FILE =
|
|
145
|
+
env.LOG_FILE = join(LOGS_DIR, `${port}.log`);
|
|
169
146
|
}
|
|
170
147
|
|
|
171
148
|
// stdout/stderr → separate crash log (safety net for non-pino output)
|
|
@@ -174,7 +151,7 @@ async function cmdStart() {
|
|
|
174
151
|
const err = openSync(stdoutPath, "a");
|
|
175
152
|
|
|
176
153
|
const self = getSelfCommand();
|
|
177
|
-
const child = spawn(self[0]!, [...self.slice(1), "run"], {
|
|
154
|
+
const child = spawn(self[0]!, [...self.slice(1), "run", `--port=${port}`], {
|
|
178
155
|
detached: true,
|
|
179
156
|
stdio: ["ignore", out, err],
|
|
180
157
|
env,
|
|
@@ -182,52 +159,61 @@ async function cmdStart() {
|
|
|
182
159
|
});
|
|
183
160
|
child.unref();
|
|
184
161
|
|
|
185
|
-
// Save instance metadata
|
|
186
|
-
saveInstance({
|
|
187
|
-
pid: child.pid!,
|
|
188
|
-
name,
|
|
189
|
-
port,
|
|
190
|
-
logPath,
|
|
191
|
-
cwd: process.cwd(),
|
|
192
|
-
startedAt: new Date().toISOString(),
|
|
193
|
-
});
|
|
194
|
-
|
|
195
162
|
console.log(`RestBase started (PID: ${child.pid}, port: ${port})`);
|
|
196
163
|
}
|
|
197
164
|
|
|
198
|
-
/**
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
165
|
+
/**
|
|
166
|
+
* Resolve a target identifier to matching instances.
|
|
167
|
+
* Supports: "all", numeric PID, or SVR_NAME (fetched from health endpoint).
|
|
168
|
+
*/
|
|
169
|
+
async function resolveInstances(target: string): Promise<{ pid: number; port: number; name?: string }[]> {
|
|
170
|
+
const all = discoverInstances();
|
|
171
|
+
if (all.length === 0) return [];
|
|
172
|
+
if (target === "all") return all;
|
|
173
|
+
|
|
174
|
+
// Try numeric PID first
|
|
175
|
+
const num = parseInt(target, 10);
|
|
176
|
+
if (!isNaN(num)) {
|
|
177
|
+
const byPid = all.find((i) => i.pid === num);
|
|
178
|
+
if (byPid) return [byPid];
|
|
179
|
+
// Maybe it's a port number?
|
|
180
|
+
const byPort = all.find((i) => i.port === num);
|
|
181
|
+
if (byPort) return [byPort];
|
|
182
|
+
return [];
|
|
208
183
|
}
|
|
209
184
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
185
|
+
// Otherwise treat as SVR_NAME — need to fetch health for each to match
|
|
186
|
+
const matched: { pid: number; port: number; name?: string }[] = [];
|
|
187
|
+
for (const inst of all) {
|
|
188
|
+
const health = await fetchHealth(inst.port);
|
|
189
|
+
if (health?.name === target) {
|
|
190
|
+
matched.push({...inst, name: health.name});
|
|
191
|
+
}
|
|
214
192
|
}
|
|
215
|
-
|
|
193
|
+
return matched;
|
|
216
194
|
}
|
|
217
195
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
196
|
+
/** stop — by PID, name, or "all" */
|
|
197
|
+
async function cmdStop(target: string) {
|
|
198
|
+
const instances = await resolveInstances(target);
|
|
199
|
+
if (instances.length === 0) {
|
|
200
|
+
console.log(target === "all" ? "No RestBase instances found" : `No instance found matching "${target}"`);
|
|
222
201
|
return;
|
|
223
202
|
}
|
|
203
|
+
for (const inst of instances) stopOne(inst.pid, inst.port, inst.name);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function stopOne(pid: number, port?: number, name?: string) {
|
|
207
|
+
const label = [name, port ? `port ${port}` : ""].filter(Boolean).join(", ");
|
|
224
208
|
try {
|
|
225
|
-
if (isAlive(pid))
|
|
226
|
-
|
|
227
|
-
|
|
209
|
+
if (isAlive(pid)) {
|
|
210
|
+
process.kill(pid, "SIGTERM");
|
|
211
|
+
console.log(`Stopped PID ${pid}${label ? ` (${label})` : ""}`);
|
|
212
|
+
} else {
|
|
213
|
+
console.log(`Process ${pid} is not running`);
|
|
214
|
+
}
|
|
228
215
|
} catch {
|
|
229
|
-
|
|
230
|
-
console.log(`Process ${pid} already gone, cleaned up`);
|
|
216
|
+
console.log(`Failed to stop PID ${pid}`);
|
|
231
217
|
}
|
|
232
218
|
}
|
|
233
219
|
|
|
@@ -271,7 +257,7 @@ function fmtTime(iso: string): string {
|
|
|
271
257
|
|
|
272
258
|
/** status — table with health check */
|
|
273
259
|
async function cmdStatus() {
|
|
274
|
-
const instances =
|
|
260
|
+
const instances = discoverInstances();
|
|
275
261
|
if (instances.length === 0) {
|
|
276
262
|
console.log("No RestBase instances found");
|
|
277
263
|
return;
|
|
@@ -294,24 +280,18 @@ async function cmdStatus() {
|
|
|
294
280
|
console.log("─".repeat(nW + pW + sW + ptW + stW + uW + mW + cW + 30));
|
|
295
281
|
|
|
296
282
|
for (const inst of instances) {
|
|
297
|
-
// Dead process → clean up silently
|
|
298
|
-
if (!isAlive(inst.pid)) {
|
|
299
|
-
removeInstance(inst.pid);
|
|
300
|
-
continue;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
283
|
// Fetch live info from health endpoint
|
|
304
284
|
const health = await fetchHealth(inst.port);
|
|
305
285
|
|
|
306
|
-
const name = health?.name ||
|
|
286
|
+
const name = health?.name || "-";
|
|
307
287
|
const state = health?.status ?? "unreachable";
|
|
308
|
-
const started = health?.startedAt ? fmtTime(health.startedAt) :
|
|
288
|
+
const started = health?.startedAt ? fmtTime(health.startedAt) : "-";
|
|
309
289
|
const uptime = health?.uptime !== undefined ? fmtUptime(health.uptime) : "-";
|
|
310
290
|
const mem = health?.memory ? fmtBytes(health.memory.rss) : "-";
|
|
311
291
|
const cpu = (health?.cpu && health?.uptime)
|
|
312
292
|
? fmtCpu(health.cpu.user, health.cpu.system, health.uptime)
|
|
313
293
|
: "-";
|
|
314
|
-
const logPath = health?.logFile ||
|
|
294
|
+
const logPath = health?.logFile || "-";
|
|
315
295
|
|
|
316
296
|
console.log(
|
|
317
297
|
name.padEnd(nW) +
|
|
@@ -327,55 +307,36 @@ async function cmdStatus() {
|
|
|
327
307
|
}
|
|
328
308
|
}
|
|
329
309
|
|
|
330
|
-
/**
|
|
331
|
-
* Find the latest log file matching the base path.
|
|
332
|
-
* pino-roll creates files like: app.2026-02-11.1.log (date + sequence inserted before extension)
|
|
333
|
-
* So for base "log/app.log", we look for "app*.log" in "log/".
|
|
334
|
-
*/
|
|
335
|
-
function findLatestLog(basePath: string): string | null {
|
|
336
|
-
// If exact file exists, use it
|
|
337
|
-
if (existsSync(basePath)) return basePath;
|
|
338
|
-
|
|
339
|
-
const dir = resolve(basePath, "..");
|
|
340
|
-
if (!existsSync(dir)) return null;
|
|
341
|
-
|
|
342
|
-
const base = basePath.split("/").pop()!; // "app.log"
|
|
343
|
-
const dot = base.lastIndexOf(".");
|
|
344
|
-
const stem = dot > 0 ? base.slice(0, dot) : base; // "app"
|
|
345
|
-
const ext = dot > 0 ? base.slice(dot) : ""; // ".log"
|
|
346
|
-
|
|
347
|
-
// Find all matching files, sort by mtime descending
|
|
348
|
-
const files = readdirSync(dir)
|
|
349
|
-
.filter((f) => f.startsWith(stem) && f.endsWith(ext) && f !== base)
|
|
350
|
-
.map((f) => ({name: f, mtime: Bun.file(join(dir, f)).lastModified}))
|
|
351
|
-
.sort((a, b) => b.mtime - a.mtime);
|
|
352
|
-
|
|
353
|
-
return files.length > 0 ? join(dir, files[0]!.name) : null;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
/** log — tail -f the log file of an instance */
|
|
310
|
+
/** log — tail -f the log file of an instance (by PID or name) */
|
|
357
311
|
async function cmdLog(target: string) {
|
|
358
|
-
const
|
|
359
|
-
if (
|
|
360
|
-
console.error(`
|
|
312
|
+
const instances = await resolveInstances(target);
|
|
313
|
+
if (instances.length === 0) {
|
|
314
|
+
console.error(`No running RestBase instance found matching "${target}"`);
|
|
315
|
+
process.exit(1);
|
|
316
|
+
}
|
|
317
|
+
if (instances.length > 1) {
|
|
318
|
+
console.error(`Multiple instances match "${target}". Use PID to be specific:`);
|
|
319
|
+
for (const i of instances) console.error(` PID ${i.pid} (port ${i.port})`);
|
|
361
320
|
process.exit(1);
|
|
362
321
|
}
|
|
363
322
|
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
323
|
+
const inst = instances[0]!;
|
|
324
|
+
|
|
325
|
+
// Fetch log file path from health endpoint (points to current.log symlink)
|
|
326
|
+
const health = await fetchHealth(inst.port);
|
|
327
|
+
if (!health?.logFile) {
|
|
328
|
+
console.error(`Cannot determine log file for PID ${inst.pid} (health endpoint unreachable or logFile not configured)`);
|
|
367
329
|
process.exit(1);
|
|
368
330
|
}
|
|
369
331
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
console.error(`Log file not found for base path: ${meta.logPath}`);
|
|
332
|
+
if (!existsSync(health.logFile)) {
|
|
333
|
+
console.error(`Log file not found: ${health.logFile}`);
|
|
373
334
|
process.exit(1);
|
|
374
335
|
}
|
|
375
336
|
|
|
376
|
-
console.log(`Tailing ${logFile} (Ctrl+C to stop)\n`);
|
|
337
|
+
console.log(`Tailing ${health.logFile} (Ctrl+C to stop)\n`);
|
|
377
338
|
|
|
378
|
-
const tail = spawn("tail", ["-f", "-n", "100", logFile], {
|
|
339
|
+
const tail = spawn("tail", ["-f", "-n", "100", health.logFile], {
|
|
379
340
|
stdio: "inherit",
|
|
380
341
|
});
|
|
381
342
|
|
|
@@ -387,54 +348,195 @@ async function cmdLog(target: string) {
|
|
|
387
348
|
await new Promise<void>((resolve) => tail.on("close", () => resolve()));
|
|
388
349
|
}
|
|
389
350
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
351
|
+
/* ═══════════ Interactive env configuration ═══════════ */
|
|
352
|
+
|
|
353
|
+
/** Parse an existing .env file into a key→value map */
|
|
354
|
+
function parseEnvFile(path: string): Record<string, string> {
|
|
355
|
+
const map: Record<string, string> = {};
|
|
356
|
+
if (!existsSync(path)) return map;
|
|
357
|
+
for (const line of readFileSync(path, "utf-8").split("\n")) {
|
|
358
|
+
const trimmed = line.trim();
|
|
359
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
360
|
+
const eq = trimmed.indexOf("=");
|
|
361
|
+
if (eq < 0) continue;
|
|
362
|
+
const key = trimmed.slice(0, eq).trim();
|
|
363
|
+
let val = trimmed.slice(eq + 1).trim();
|
|
364
|
+
// strip surrounding quotes
|
|
365
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
366
|
+
val = val.slice(1, -1);
|
|
367
|
+
}
|
|
368
|
+
map[key] = val;
|
|
397
369
|
}
|
|
370
|
+
return map;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** Interactive env — prompt user for each config parameter */
|
|
374
|
+
async function cmdEnv() {
|
|
375
|
+
const envPath = resolve(process.cwd(), ".env");
|
|
376
|
+
const existing = parseEnvFile(envPath);
|
|
377
|
+
const isUpdate = Object.keys(existing).length > 0;
|
|
398
378
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
379
|
+
p.intro(isUpdate ? "Reconfigure .env (current values as defaults)" : "Initialize .env configuration");
|
|
380
|
+
|
|
381
|
+
/* helper: get default — existing value > built-in default */
|
|
382
|
+
const d = (key: string, fallback: string) => existing[key] ?? fallback;
|
|
383
|
+
|
|
384
|
+
/* helper: wrap clack text — returns string (empty if user skipped) */
|
|
385
|
+
const ask = async (opts: { message: string; key: string; fallback: string; placeholder?: string }) => {
|
|
386
|
+
const result = await p.text({
|
|
387
|
+
message: opts.message,
|
|
388
|
+
initialValue: d(opts.key, opts.fallback),
|
|
389
|
+
placeholder: opts.placeholder,
|
|
390
|
+
});
|
|
391
|
+
if (p.isCancel(result)) { p.cancel("Cancelled"); process.exit(0); }
|
|
392
|
+
return (result as string).trim();
|
|
393
|
+
};
|
|
406
394
|
|
|
407
|
-
|
|
395
|
+
/* helper: select */
|
|
396
|
+
const choose = async (opts: { message: string; key: string; fallback: string; options: { value: string; label: string }[] }) => {
|
|
397
|
+
const result = await p.select({
|
|
398
|
+
message: opts.message,
|
|
399
|
+
initialValue: d(opts.key, opts.fallback),
|
|
400
|
+
options: opts.options,
|
|
401
|
+
});
|
|
402
|
+
if (p.isCancel(result)) { p.cancel("Cancelled"); process.exit(0); }
|
|
403
|
+
return result as string;
|
|
404
|
+
};
|
|
408
405
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
# SVR_STATIC= # Static file directory for SPA hosting
|
|
412
|
-
# SVR_API_LIMIT=100 # Rate limit: max requests per second per API
|
|
406
|
+
// ── Server ──
|
|
407
|
+
p.log.step("Server");
|
|
413
408
|
|
|
414
|
-
|
|
409
|
+
const SVR_NAME = await ask({
|
|
410
|
+
message: "Instance name (shown in 'restbase status')",
|
|
411
|
+
key: "SVR_NAME", fallback: "", placeholder: "optional",
|
|
412
|
+
});
|
|
413
|
+
const SVR_PORT = await ask({
|
|
414
|
+
message: "Server port",
|
|
415
|
+
key: "SVR_PORT", fallback: "3333",
|
|
416
|
+
});
|
|
417
|
+
const SVR_STATIC = await ask({
|
|
418
|
+
message: "Static file directory for SPA hosting",
|
|
419
|
+
key: "SVR_STATIC", fallback: "", placeholder: "optional, e.g. dist",
|
|
420
|
+
});
|
|
421
|
+
const SVR_API_LIMIT = await ask({
|
|
422
|
+
message: "Rate limit (max requests per second per API, 0 = off)",
|
|
423
|
+
key: "SVR_API_LIMIT", fallback: "100",
|
|
424
|
+
});
|
|
415
425
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
# DB_AUTH_FIELD=owner # Owner field name for tenant isolation
|
|
419
|
-
# DB_AUTH_FIELD_NULL_OPEN=false # Treat owner=NULL rows as public data
|
|
420
|
-
# DB_INIT_SQL= # SQL file to run on startup
|
|
426
|
+
// ── Database ──
|
|
427
|
+
p.log.step("Database");
|
|
421
428
|
|
|
422
|
-
|
|
429
|
+
const DB_URL = await ask({
|
|
430
|
+
message: "Database URL (sqlite://<path> or mysql://user:pass@host/db)",
|
|
431
|
+
key: "DB_URL", fallback: "sqlite://:memory:",
|
|
432
|
+
});
|
|
433
|
+
const DB_AUTH_TABLE = await ask({
|
|
434
|
+
message: "Auth table name",
|
|
435
|
+
key: "DB_AUTH_TABLE", fallback: "users",
|
|
436
|
+
});
|
|
437
|
+
const DB_AUTH_FIELD = await ask({
|
|
438
|
+
message: "Owner field name (tenant isolation)",
|
|
439
|
+
key: "DB_AUTH_FIELD", fallback: "owner",
|
|
440
|
+
});
|
|
441
|
+
const DB_AUTH_FIELD_NULL_OPEN = await choose({
|
|
442
|
+
message: "Treat owner=NULL rows as public data?",
|
|
443
|
+
key: "DB_AUTH_FIELD_NULL_OPEN", fallback: "false",
|
|
444
|
+
options: [
|
|
445
|
+
{value: "false", label: "false — NULL rows are hidden"},
|
|
446
|
+
{value: "true", label: "true — NULL rows are visible to everyone"},
|
|
447
|
+
],
|
|
448
|
+
});
|
|
449
|
+
const DB_INIT_SQL = await ask({
|
|
450
|
+
message: "SQL file to run on startup",
|
|
451
|
+
key: "DB_INIT_SQL", fallback: "", placeholder: "optional, e.g. init.sql",
|
|
452
|
+
});
|
|
423
453
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
# AUTH_BASIC_OPEN=true # Enable Basic Auth
|
|
454
|
+
// ── Auth ──
|
|
455
|
+
p.log.step("Auth");
|
|
427
456
|
|
|
428
|
-
|
|
457
|
+
const AUTH_JWT_SECRET = await ask({
|
|
458
|
+
message: "JWT secret (⚠ change in production!)",
|
|
459
|
+
key: "AUTH_JWT_SECRET", fallback: "restbase",
|
|
460
|
+
});
|
|
461
|
+
const AUTH_JWT_EXP = await ask({
|
|
462
|
+
message: "JWT expiry in seconds (default: 12 hours)",
|
|
463
|
+
key: "AUTH_JWT_EXP", fallback: "43200",
|
|
464
|
+
});
|
|
465
|
+
const AUTH_BASIC_OPEN = await choose({
|
|
466
|
+
message: "Enable Basic Auth?",
|
|
467
|
+
key: "AUTH_BASIC_OPEN", fallback: "true",
|
|
468
|
+
options: [
|
|
469
|
+
{value: "true", label: "true — Basic Auth enabled"},
|
|
470
|
+
{value: "false", label: "false — JWT only"},
|
|
471
|
+
],
|
|
472
|
+
});
|
|
429
473
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
474
|
+
// ── Logging ──
|
|
475
|
+
p.log.step("Logging");
|
|
476
|
+
|
|
477
|
+
const LOG_LEVEL = await choose({
|
|
478
|
+
message: "Log level",
|
|
479
|
+
key: "LOG_LEVEL", fallback: "INFO",
|
|
480
|
+
options: [
|
|
481
|
+
{value: "ERROR", label: "ERROR — errors only"},
|
|
482
|
+
{value: "INFO", label: "INFO — requests + errors"},
|
|
483
|
+
{value: "DEBUG", label: "DEBUG — verbose (headers, body, SQL)"},
|
|
484
|
+
],
|
|
485
|
+
});
|
|
486
|
+
const LOG_CONSOLE = await choose({
|
|
487
|
+
message: "Console output? (auto-disabled in daemon mode)",
|
|
488
|
+
key: "LOG_CONSOLE", fallback: "true",
|
|
489
|
+
options: [
|
|
490
|
+
{value: "true", label: "true — print to console"},
|
|
491
|
+
{value: "false", label: "false — silent"},
|
|
492
|
+
],
|
|
493
|
+
});
|
|
494
|
+
const LOG_FILE = await ask({
|
|
495
|
+
message: "Log file path (auto-configured in daemon mode)",
|
|
496
|
+
key: "LOG_FILE", fallback: "", placeholder: "optional, e.g. log/app.log",
|
|
497
|
+
});
|
|
498
|
+
const LOG_RETAIN_DAYS = await ask({
|
|
499
|
+
message: "Log file retention days",
|
|
500
|
+
key: "LOG_RETAIN_DAYS", fallback: "7",
|
|
501
|
+
});
|
|
435
502
|
|
|
436
|
-
|
|
437
|
-
|
|
503
|
+
// ── Build .env content ──
|
|
504
|
+
const line = (key: string, val: string, comment: string) =>
|
|
505
|
+
val ? `${key}=${val}${comment ? ` # ${comment}` : ""}` : `# ${key}=${comment ? ` # ${comment}` : ""}`;
|
|
506
|
+
|
|
507
|
+
const lines = [
|
|
508
|
+
"# ═══════════════════════════════════════════════════════════",
|
|
509
|
+
"# RestBase Configuration",
|
|
510
|
+
"# ═══════════════════════════════════════════════════════════",
|
|
511
|
+
"",
|
|
512
|
+
"# ── Server ────────────────────────────────────────────────",
|
|
513
|
+
line("SVR_NAME", SVR_NAME, "Instance name"),
|
|
514
|
+
line("SVR_PORT", SVR_PORT, "Server port"),
|
|
515
|
+
line("SVR_STATIC", SVR_STATIC, "Static file directory"),
|
|
516
|
+
line("SVR_API_LIMIT", SVR_API_LIMIT, "Rate limit per API"),
|
|
517
|
+
"",
|
|
518
|
+
"# ── Database ──────────────────────────────────────────────",
|
|
519
|
+
line("DB_URL", DB_URL, ""),
|
|
520
|
+
line("DB_AUTH_TABLE", DB_AUTH_TABLE, "Auth table name"),
|
|
521
|
+
line("DB_AUTH_FIELD", DB_AUTH_FIELD, "Owner field"),
|
|
522
|
+
line("DB_AUTH_FIELD_NULL_OPEN", DB_AUTH_FIELD_NULL_OPEN, ""),
|
|
523
|
+
line("DB_INIT_SQL", DB_INIT_SQL, "SQL file on startup"),
|
|
524
|
+
"",
|
|
525
|
+
"# ── Auth ──────────────────────────────────────────────────",
|
|
526
|
+
line("AUTH_JWT_SECRET", AUTH_JWT_SECRET, "⚠ change in production"),
|
|
527
|
+
line("AUTH_JWT_EXP", AUTH_JWT_EXP, "seconds"),
|
|
528
|
+
line("AUTH_BASIC_OPEN", AUTH_BASIC_OPEN, ""),
|
|
529
|
+
"",
|
|
530
|
+
"# ── Logging ───────────────────────────────────────────────",
|
|
531
|
+
line("LOG_LEVEL", LOG_LEVEL, "ERROR / INFO / DEBUG"),
|
|
532
|
+
line("LOG_CONSOLE", LOG_CONSOLE, ""),
|
|
533
|
+
line("LOG_FILE", LOG_FILE, "auto-configured in daemon mode"),
|
|
534
|
+
line("LOG_RETAIN_DAYS", LOG_RETAIN_DAYS, "days"),
|
|
535
|
+
"",
|
|
536
|
+
];
|
|
537
|
+
|
|
538
|
+
writeFileSync(envPath, lines.join("\n"));
|
|
539
|
+
p.outro(`Saved to ${envPath}`);
|
|
438
540
|
}
|
|
439
541
|
|
|
440
542
|
/** version */
|
|
@@ -454,10 +556,10 @@ Usage:
|
|
|
454
556
|
Commands:
|
|
455
557
|
run Start server in foreground (reads .env)
|
|
456
558
|
start Start server in background (daemon mode)
|
|
457
|
-
stop <pid|all> Stop
|
|
458
|
-
status
|
|
459
|
-
log <pid> Tail the log of
|
|
460
|
-
env
|
|
559
|
+
stop <pid|name|all> Stop instance(s) by PID, SVR_NAME, or all
|
|
560
|
+
status Show all running background instances
|
|
561
|
+
log <pid|name> Tail the log of an instance by PID or SVR_NAME
|
|
562
|
+
env Interactive .env configuration (create or reconfigure)
|
|
461
563
|
version Show version
|
|
462
564
|
help Show this help
|
|
463
565
|
|
|
@@ -465,16 +567,20 @@ Examples:
|
|
|
465
567
|
restbase run Start in foreground
|
|
466
568
|
restbase start Start in background (daemon)
|
|
467
569
|
restbase status List running instances with health status
|
|
468
|
-
restbase log 12345 Tail log
|
|
469
|
-
restbase
|
|
570
|
+
restbase log 12345 Tail log by PID
|
|
571
|
+
restbase log my-api Tail log by SVR_NAME
|
|
572
|
+
restbase stop 12345 Stop instance by PID
|
|
573
|
+
restbase stop my-api Stop instance by SVR_NAME
|
|
470
574
|
restbase stop all Stop all background instances
|
|
471
|
-
restbase env
|
|
575
|
+
restbase env Interactive .env setup (reconfigure if exists)
|
|
472
576
|
|
|
473
577
|
Configuration:
|
|
474
578
|
All settings are read from .env in the current working directory.
|
|
475
|
-
Run 'restbase env'
|
|
579
|
+
Run 'restbase env' for interactive configuration (create or update .env).
|
|
476
580
|
|
|
477
|
-
Instance
|
|
581
|
+
Instance discovery:
|
|
582
|
+
Background instances are discovered via 'ps' — no PID files needed.
|
|
583
|
+
Instance details (name, log path, uptime…) are fetched from /api/health.
|
|
478
584
|
`);
|
|
479
585
|
}
|
|
480
586
|
|
|
@@ -495,10 +601,10 @@ switch (command) {
|
|
|
495
601
|
|
|
496
602
|
case "stop":
|
|
497
603
|
if (!arg1) {
|
|
498
|
-
console.error("Usage: restbase stop <pid|all>");
|
|
604
|
+
console.error("Usage: restbase stop <pid|name|all>");
|
|
499
605
|
process.exit(1);
|
|
500
606
|
}
|
|
501
|
-
cmdStop(arg1);
|
|
607
|
+
await cmdStop(arg1);
|
|
502
608
|
process.exit(0);
|
|
503
609
|
break;
|
|
504
610
|
|
|
@@ -509,14 +615,14 @@ switch (command) {
|
|
|
509
615
|
|
|
510
616
|
case "log":
|
|
511
617
|
if (!arg1) {
|
|
512
|
-
console.error("Usage: restbase log <pid>");
|
|
618
|
+
console.error("Usage: restbase log <pid|name>");
|
|
513
619
|
process.exit(1);
|
|
514
620
|
}
|
|
515
621
|
await cmdLog(arg1);
|
|
516
622
|
break;
|
|
517
623
|
|
|
518
624
|
case "env":
|
|
519
|
-
cmdEnv();
|
|
625
|
+
await cmdEnv();
|
|
520
626
|
process.exit(0);
|
|
521
627
|
break;
|
|
522
628
|
|
package/client/README.md
CHANGED
|
@@ -51,7 +51,7 @@ import RestBase, {
|
|
|
51
51
|
```ts
|
|
52
52
|
import RestBase, { eq, gt, or, agg, sel } from "@dtdyq/restbase-client";
|
|
53
53
|
|
|
54
|
-
//
|
|
54
|
+
// 同源部署 — 不需要传 endpoint
|
|
55
55
|
const rb = new RestBase();
|
|
56
56
|
|
|
57
57
|
// 或指定后端地址
|
|
@@ -67,6 +67,16 @@ const list = await products.query()
|
|
|
67
67
|
.orderDesc("price")
|
|
68
68
|
.page(1, 20)
|
|
69
69
|
.data();
|
|
70
|
+
|
|
71
|
+
// ── 多节点负载均衡 ──
|
|
72
|
+
// 所有节点连同一个数据库,请求随机分发
|
|
73
|
+
const rb2 = new RestBase([
|
|
74
|
+
"http://node1:3333",
|
|
75
|
+
"http://node2:3333",
|
|
76
|
+
"http://node3:3333",
|
|
77
|
+
]);
|
|
78
|
+
await rb2.auth.login("admin", "admin"); // 登录一次,token 共享
|
|
79
|
+
const list2 = await rb2.table("products").query().data(); // 随机打到某个节点
|
|
70
80
|
```
|
|
71
81
|
|
|
72
82
|
---
|
|
@@ -74,20 +84,32 @@ const list = await products.query()
|
|
|
74
84
|
## RestBase — 主入口
|
|
75
85
|
|
|
76
86
|
```ts
|
|
77
|
-
|
|
78
|
-
const rb = new RestBase(
|
|
87
|
+
// 同源部署(Vite proxy / 静态托管)
|
|
88
|
+
const rb = new RestBase();
|
|
89
|
+
|
|
90
|
+
// 单个 endpoint
|
|
91
|
+
const rb = new RestBase("http://localhost:3333");
|
|
92
|
+
|
|
93
|
+
// 多个 endpoint — 负载均衡(每次请求随机选一个节点)
|
|
94
|
+
const rb = new RestBase([
|
|
95
|
+
"http://localhost:3333",
|
|
96
|
+
"http://localhost:8080",
|
|
97
|
+
"http://localhost:9090",
|
|
98
|
+
]);
|
|
79
99
|
```
|
|
80
100
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
|
84
|
-
|
|
85
|
-
| `rb.
|
|
86
|
-
| `rb.
|
|
87
|
-
| `rb.
|
|
88
|
-
| `rb.
|
|
89
|
-
| `rb.
|
|
90
|
-
| `rb.
|
|
101
|
+
> 多 endpoint 模式要求所有服务端实例连接同一个数据库。客户端共享同一套 auth 状态(token / Basic Auth),每次请求随机分发到不同节点,分散单节点压力。
|
|
102
|
+
|
|
103
|
+
| 方法 | 返回类型 | 说明 |
|
|
104
|
+
|:----------------------|:------------------------------------------|:------------------------------------|
|
|
105
|
+
| `rb.auth` | `AuthClient` | 鉴权客户端 |
|
|
106
|
+
| `rb.table<T>(name)` | `TableClient<T>` | 获取表操作客户端 |
|
|
107
|
+
| `rb.health()` | `Promise<ApiResponse>` | 健康检查(返回 name/port/pid/uptime/mem/cpu) |
|
|
108
|
+
| `rb.tables()` | `Promise<ApiResponse<TableMeta[]>>` | 获取所有表元数据(不含 users) |
|
|
109
|
+
| `rb.tableMeta(name)` | `Promise<ApiResponse<TableMeta \| null>>` | 获取单表元数据 |
|
|
110
|
+
| `rb.syncMeta()` | `Promise<ApiResponse<TableMeta[]>>` | 运行时同步 DB 表结构 |
|
|
111
|
+
| `rb.setHeader(k, v)` | `this` | 设置自定义请求头 |
|
|
112
|
+
| `rb.setRequestId(id)` | `this` | 设置请求追踪 ID(`X-Request-Id`) |
|
|
91
113
|
|
|
92
114
|
**TableMeta 结构:**
|
|
93
115
|
|
package/client/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dtdyq/restbase-client",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "2.1.0",
|
|
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",
|
|
@@ -451,14 +451,20 @@ export class AuthClient {
|
|
|
451
451
|
══════════════════════════════════════════════════════════════ */
|
|
452
452
|
|
|
453
453
|
class HttpClient {
|
|
454
|
-
private
|
|
454
|
+
private _urls: string[];
|
|
455
455
|
private _token: string | null = null;
|
|
456
456
|
private _basicAuth: string | null = null;
|
|
457
457
|
private _requestId?: string;
|
|
458
458
|
private _headers: Record<string, string> = {};
|
|
459
459
|
|
|
460
|
-
constructor(
|
|
461
|
-
|
|
460
|
+
constructor(urls: string | string[]) {
|
|
461
|
+
const list = Array.isArray(urls) ? urls : [urls];
|
|
462
|
+
this._urls = list.map((u) => u.replace(/\/+$/, ""));
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** Pick a random base URL (load balancing) */
|
|
466
|
+
private _pickUrl(): string {
|
|
467
|
+
return this._urls[Math.floor(Math.random() * this._urls.length)]!;
|
|
462
468
|
}
|
|
463
469
|
|
|
464
470
|
setToken(token: string | null): void {
|
|
@@ -511,7 +517,7 @@ class HttpClient {
|
|
|
511
517
|
}
|
|
512
518
|
|
|
513
519
|
private async _fetch<T>(method: string, path: string, params?: Record<string, string>, body?: unknown): Promise<ApiResponse<T>> {
|
|
514
|
-
let url = `${this.
|
|
520
|
+
let url = `${this._pickUrl()}${path}`;
|
|
515
521
|
if (params && Object.keys(params).length > 0) {
|
|
516
522
|
url += `?${new URLSearchParams(params).toString()}`;
|
|
517
523
|
}
|
|
@@ -526,12 +532,35 @@ class HttpClient {
|
|
|
526
532
|
RestBase — 主入口
|
|
527
533
|
══════════════════════════════════════════════════════════════ */
|
|
528
534
|
|
|
535
|
+
/**
|
|
536
|
+
* RestBase — 前端客户端主入口
|
|
537
|
+
*
|
|
538
|
+
* 支持三种构造方式:
|
|
539
|
+
*
|
|
540
|
+
* ```ts
|
|
541
|
+
* // 1. 同源部署(无参数)
|
|
542
|
+
* const rb = new RestBase();
|
|
543
|
+
*
|
|
544
|
+
* // 2. 单个 endpoint
|
|
545
|
+
* const rb = new RestBase("http://localhost:3333");
|
|
546
|
+
*
|
|
547
|
+
* // 3. 多个 endpoint(负载均衡,每次请求随机选一个)
|
|
548
|
+
* const rb = new RestBase([
|
|
549
|
+
* "http://localhost:3333",
|
|
550
|
+
* "http://localhost:8080",
|
|
551
|
+
* "http://localhost:9090",
|
|
552
|
+
* ]);
|
|
553
|
+
* ```
|
|
554
|
+
*
|
|
555
|
+
* 多 endpoint 模式下,所有服务端实例应连接同一个数据库。
|
|
556
|
+
* 客户端共享同一套 auth 状态,每次请求随机分发到不同节点。
|
|
557
|
+
*/
|
|
529
558
|
export class RestBase {
|
|
530
559
|
readonly auth: AuthClient;
|
|
531
560
|
private _http: HttpClient;
|
|
532
561
|
|
|
533
|
-
constructor(endpoint
|
|
534
|
-
this._http = new HttpClient(endpoint);
|
|
562
|
+
constructor(endpoint?: string | string[]) {
|
|
563
|
+
this._http = new HttpClient(endpoint ?? "");
|
|
535
564
|
this.auth = new AuthClient(this._http);
|
|
536
565
|
}
|
|
537
566
|
|
|
@@ -541,8 +570,8 @@ export class RestBase {
|
|
|
541
570
|
}
|
|
542
571
|
|
|
543
572
|
/** 健康检查 */
|
|
544
|
-
async health(): Promise<ApiResponse
|
|
545
|
-
return this._http.get
|
|
573
|
+
async health(): Promise<ApiResponse> {
|
|
574
|
+
return this._http.get("/api/health");
|
|
546
575
|
}
|
|
547
576
|
|
|
548
577
|
/** 获取所有表元数据(不含 users 表) */
|
|
@@ -560,13 +589,13 @@ export class RestBase {
|
|
|
560
589
|
return this._http.get<TableMeta[]>("/api/meta/sync");
|
|
561
590
|
}
|
|
562
591
|
|
|
563
|
-
/**
|
|
592
|
+
/** 设置自定义请求头(当前 endpoint) */
|
|
564
593
|
setHeader(key: string, value: string): this {
|
|
565
594
|
this._http.setHeader(key, value);
|
|
566
595
|
return this;
|
|
567
596
|
}
|
|
568
597
|
|
|
569
|
-
/** 设置请求追踪 ID */
|
|
598
|
+
/** 设置请求追踪 ID(当前 endpoint) */
|
|
570
599
|
setRequestId(id: string): this {
|
|
571
600
|
this._http.setRequestId(id);
|
|
572
601
|
return this;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dtdyq/restbase",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Zero-code REST API server for SQLite / MySQL — CLI with daemon mode, health monitoring, built-in auth & tenant isolation",
|
|
5
5
|
"keywords": ["rest", "api", "crud", "sqlite", "mysql", "hono", "bun", "zero-code", "cli", "daemon"],
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,10 +18,12 @@
|
|
|
18
18
|
},
|
|
19
19
|
"scripts": {
|
|
20
20
|
"dev": "bun run --watch src/server.ts",
|
|
21
|
+
"start": "bun run src/server.ts",
|
|
21
22
|
"test": "bun test src/rest.test.ts",
|
|
22
23
|
"build": "bun build --compile --minify --sourcemap ./bin/restbase.ts --outfile restbase"
|
|
23
24
|
},
|
|
24
25
|
"dependencies": {
|
|
26
|
+
"@clack/prompts": "^1.0.0",
|
|
25
27
|
"@hono/zod-validator": "^0.4.0",
|
|
26
28
|
"hono": "^4.6.0",
|
|
27
29
|
"pino": "^10.3.1",
|
package/src/logger.ts
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* - 日志保留天数(LOG_RETAIN_DAYS,默认 7)
|
|
8
8
|
* - 日志等级(LOG_LEVEL)
|
|
9
9
|
*
|
|
10
|
+
* pino-roll 启用 symlink,当前日志始终通过 current.log 软链接访问。
|
|
11
|
+
*
|
|
10
12
|
* 导出 log 实例,全局使用 log.info / log.debug / log.error / log.fatal
|
|
11
13
|
*/
|
|
12
14
|
import pino from "pino";
|
|
@@ -57,7 +59,7 @@ if (cfg.logConsole) {
|
|
|
57
59
|
});
|
|
58
60
|
}
|
|
59
61
|
|
|
60
|
-
/* 文件(pino-roll 滚动写入 NDJSON
|
|
62
|
+
/* 文件(pino-roll 滚动写入 NDJSON,symlink 指向当前文件) */
|
|
61
63
|
if (cfg.logFile) {
|
|
62
64
|
const {default: buildRollStream} = await import("pino-roll");
|
|
63
65
|
const rollStream = await buildRollStream({
|
|
@@ -67,6 +69,7 @@ if (cfg.logFile) {
|
|
|
67
69
|
dateFormat: "yyyy-MM-dd",
|
|
68
70
|
limit: {count: cfg.logRetainDays},
|
|
69
71
|
mkdir: true,
|
|
72
|
+
symlink: true,
|
|
70
73
|
});
|
|
71
74
|
|
|
72
75
|
streams.push({
|
package/src/server.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {Hono} from "hono";
|
|
|
21
21
|
import {requestId} from "hono/request-id";
|
|
22
22
|
import {cors} from "hono/cors";
|
|
23
23
|
import {serveStatic} from "hono/bun";
|
|
24
|
+
import {resolve, dirname} from "path";
|
|
24
25
|
import {type ApiRes, type AppEnv, AppError, cfg, ok, reqStore} from "./types.ts";
|
|
25
26
|
import {log} from "./logger.ts";
|
|
26
27
|
import {getTableMetaByName, getTablesMeta, initDb, syncTablesMeta} from "./db.ts";
|
|
@@ -173,7 +174,8 @@ app.get("/api/health", (c) => {
|
|
|
173
174
|
name: cfg.name || undefined,
|
|
174
175
|
port: cfg.port,
|
|
175
176
|
pid: process.pid,
|
|
176
|
-
|
|
177
|
+
cwd: process.cwd(),
|
|
178
|
+
logFile: cfg.logFile ? resolve(dirname(cfg.logFile), "current.log") : undefined,
|
|
177
179
|
startedAt,
|
|
178
180
|
uptime: uptimeSec,
|
|
179
181
|
memory: {
|