@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/server.ts ADDED
@@ -0,0 +1,201 @@
1
+ /**
2
+ * server.ts — 入口文件
3
+ *
4
+ * 职责:
5
+ * 1. 安全检查
6
+ * 2. 初始化数据库(失败终止)
7
+ * 3. 创建 Hono 应用 / CORS 中间件
8
+ * 4. requestId 中间件(自动生成或读取 X-Request-Id)
9
+ * 5. API 限流中间件(令牌桶算法,每秒每 API)
10
+ * 6. 最外层日志中间件(INFO: requestId/method/path/ms;DEBUG: +headers/body/response/SQL)
11
+ * 7. 全局错误兜底(任何异常统一返回 HTTP 200 + 标准 JSON)
12
+ * 8. 健康检查接口
13
+ * 9. 鉴权中间件(JWT + Basic Auth)
14
+ * 10. 注册 auth / CRUD 路由 / 元数据接口
15
+ * 11. 静态文件托管(可选)
16
+ * 12. 启动 Bun HTTP 服务
17
+ *
18
+ * 启动命令: bun run server.ts
19
+ */
20
+ import {Hono} from "hono";
21
+ import {requestId} from "hono/request-id";
22
+ import {cors} from "hono/cors";
23
+ import {serveStatic} from "hono/bun";
24
+ import {type ApiRes, type AppEnv, AppError, cfg, ok, reqStore} from "./types.ts";
25
+ import {log} from "./logger.ts";
26
+ import {getTableMetaByName, getTablesMeta, initDb, syncTablesMeta} from "./db.ts";
27
+ import {authMiddleware, registerAuthRoutes} from "./auth.ts";
28
+ import {registerCrudRoutes} from "./crud.ts";
29
+
30
+ /* ═══════════ 1. 安全检查 ═══════════ */
31
+
32
+ if (cfg.jwtSecret === "restbase") {
33
+ log.warn("AUTH_JWT_SECRET is using default value! Set a strong secret in production.");
34
+ }
35
+
36
+ /* ═══════════ 2. 初始化数据库 ═══════════ */
37
+
38
+ try {
39
+ await initDb();
40
+ } catch (err) {
41
+ log.fatal({err}, "Database init failed");
42
+ process.exit(1);
43
+ }
44
+
45
+ /* ═══════════ 2. 创建 Hono 应用 ═══════════ */
46
+
47
+ const app = new Hono<AppEnv>();
48
+
49
+ /* ═══════════ 3. CORS 中间件 ═══════════ */
50
+
51
+ app.use("*", cors({
52
+ origin: "*", // 允许所有来源(生产环境可改为具体域名)
53
+ allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
54
+ allowHeaders: ["Content-Type", "Authorization", "X-Request-Id"],
55
+ exposeHeaders: ["X-Request-Id"],
56
+ maxAge: 86400, // preflight 缓存 24 小时
57
+ }));
58
+
59
+ /* ═══════════ 4. Request ID 中间件 ═══════════ */
60
+ /* 优先读取请求头 X-Request-Id,没有则自动生成 UUID */
61
+
62
+ app.use("*", requestId());
63
+
64
+ /* ═══════════ 5. API 限流中间件(滑动窗口,每秒每 IP) ═══════════ */
65
+
66
+ if (cfg.apiLimit > 0) {
67
+ /** API 路径 → { tokens: 剩余令牌, lastRefill: 上次填充时间戳 } */
68
+ const buckets = new Map<string, { tokens: number; lastRefill: number }>();
69
+
70
+ app.use("/api/*", async (c, next) => {
71
+ const key = `${c.req.method} ${c.req.path}`;
72
+ const now = Date.now();
73
+ let bucket = buckets.get(key);
74
+
75
+ if (!bucket) {
76
+ bucket = {tokens: cfg.apiLimit, lastRefill: now};
77
+ buckets.set(key, bucket);
78
+ }
79
+
80
+ /* 按经过的时间补充令牌(令牌桶算法) */
81
+ const elapsed = (now - bucket.lastRefill) / 1000;
82
+ bucket.tokens = Math.min(cfg.apiLimit, bucket.tokens + elapsed * cfg.apiLimit);
83
+ bucket.lastRefill = now;
84
+
85
+ if (bucket.tokens < 1) {
86
+ c.res = new Response(
87
+ JSON.stringify({code: "RATE_LIMITED", message: `Rate limit exceeded (${cfg.apiLimit} req/s)`}, null, 2),
88
+ {status: 200, headers: {"Content-Type": "application/json", "Retry-After": "1"}},
89
+ );
90
+ return;
91
+ }
92
+
93
+ bucket.tokens -= 1;
94
+ return next();
95
+ });
96
+
97
+ log.info({limit: cfg.apiLimit}, `API rate limit: ${cfg.apiLimit} req/s per API`);
98
+ }
99
+
100
+ /* ═══════════ 6. 日志中间件(最外层) ═══════════ */
101
+
102
+ app.use("*", async (c, next) => {
103
+ const start = Date.now();
104
+ const requestId = c.get("requestId");
105
+
106
+ /* 将 requestId 注入 AsyncLocalStorage,下游(如 db.run)可自动获取 */
107
+ await reqStore.run({requestId}, async () => {
108
+ /* DEBUG: 打印请求详情 */
109
+ if (cfg.logLevel === "DEBUG") {
110
+ const headers: Record<string, string> = {};
111
+ c.req.raw.headers.forEach((v, k) => (headers[k] = v));
112
+ log.debug({requestId, method: c.req.method, path: c.req.path, headers}, "← request");
113
+ if (c.req.method !== "GET" && c.req.method !== "HEAD") {
114
+ try {
115
+ log.debug({requestId, body: await c.req.raw.clone().text()}, "← body");
116
+ } catch { /* ignore */
117
+ }
118
+ }
119
+ }
120
+
121
+ await next();
122
+
123
+ /* JSON 响应默认格式化(pretty print) */
124
+ const ct = c.res.headers.get("Content-Type");
125
+ if (ct?.includes("application/json")) {
126
+ const body = await c.res.json();
127
+ c.res = new Response(JSON.stringify(body, null, 2), c.res);
128
+ }
129
+
130
+ const ms = Date.now() - start;
131
+
132
+ /* INFO / DEBUG: 打印请求摘要(含 requestId) */
133
+ if (cfg.logLevel !== "ERROR") {
134
+ log.info({requestId, method: c.req.method, path: c.req.path, ms}, "→ response");
135
+ }
136
+
137
+ /* DEBUG: 打印响应体 */
138
+ if (cfg.logLevel === "DEBUG") {
139
+ try {
140
+ log.debug({requestId, body: await c.res.clone().text()}, "→ body");
141
+ } catch { /* ignore */
142
+ }
143
+ }
144
+ });
145
+ });
146
+
147
+ /* ═══════════ 7. 全局错误兜底(始终 HTTP 200) ═══════════ */
148
+
149
+ app.onError((err, c) => {
150
+ if (err instanceof AppError) {
151
+ return c.json({code: err.code, message: err.message} as ApiRes);
152
+ }
153
+ /* 非业务错误:打印堆栈,返回通用错误码 */
154
+ log.error({err}, "Unhandled error");
155
+ return c.json({
156
+ code: "SYS_ERROR",
157
+ message: err.message || "Internal server error",
158
+ } as ApiRes);
159
+ });
160
+
161
+ /* ═══════════ 8. 健康检查(公开) ═══════════ */
162
+
163
+ app.get("/api/health", (c) => c.json(ok({status: "healthy"})));
164
+
165
+ /* ═══════════ 9. 鉴权中间件 ═══════════ */
166
+
167
+ app.use("/api/*", authMiddleware);
168
+
169
+ /* ═══════════ 10. 注册路由 ═══════════ */
170
+
171
+ registerAuthRoutes(app);
172
+ registerCrudRoutes(app);
173
+
174
+ /* ═══════════ 11. 元数据接口 ═══════════ */
175
+
176
+ app.get("/api/meta/tables", (c) => c.json(ok(getTablesMeta())));
177
+
178
+ app.get("/api/meta/tables/:name", (c) => {
179
+ const data = getTableMetaByName(c.req.param("name"));
180
+ return c.json(ok(data));
181
+ });
182
+
183
+ app.get("/api/meta/sync", async (c) => {
184
+ const data = await syncTablesMeta();
185
+ return c.json(ok(data));
186
+ });
187
+
188
+ /* ═══════════ 12. 静态文件托管(可选) ═══════════ */
189
+
190
+ if (cfg.staticDir) {
191
+ /* 静态资源 */
192
+ app.use("/*", serveStatic({root: cfg.staticDir}));
193
+ /* SPA fallback:所有未匹配路由返回 index.html */
194
+ app.use("/*", serveStatic({root: cfg.staticDir, path: "index.html"}));
195
+ log.info({dir: cfg.staticDir}, "Static file serving enabled");
196
+ }
197
+
198
+ /* ═══════════ 13. 启动服务 ═══════════ */
199
+
200
+ export const server = Bun.serve({port: cfg.port, fetch: app.fetch});
201
+ log.info({port: cfg.port}, `Server started http://localhost:${cfg.port}`);
package/types.ts ADDED
@@ -0,0 +1,186 @@
1
+ /**
2
+ * types.ts — 全局配置、类型定义、统一响应、Zod 校验、AppError
3
+ *
4
+ * 所有 .env 变量均有默认值(约定大于配置)
5
+ */
6
+ import {z} from "zod";
7
+ import {AsyncLocalStorage} from "node:async_hooks";
8
+
9
+ /* ═══════════ .env 配置 ═══════════ */
10
+
11
+ const _db = Bun.env.DB_URL ?? "sqlite://:memory:";
12
+
13
+ export const cfg = {
14
+ /** 服务端口 */
15
+ port: Number(Bun.env.SVR_PORT) || 3333,
16
+ /** 静态文件目录(相对路径,为空则不托管) */
17
+ staticDir: Bun.env.SVR_STATIC ?? "",
18
+ /** API 限流:每秒每个 API 接口允许的最大请求数(0 = 不限流) */
19
+ apiLimit: Number(Bun.env.SVR_API_LIMIT) || 100,
20
+ /** 数据库连接字符串 */
21
+ db: _db,
22
+ /** 是否 SQLite(非 mysql:// 则视为 SQLite) */
23
+ isSqlite: !_db.startsWith("mysql"),
24
+ /** 用户认证表名 */
25
+ authTable: Bun.env.DB_AUTH_TABLE ?? "users",
26
+ /** 数据表中的 owner 字段名 */
27
+ ownerField: Bun.env.DB_AUTH_FIELD ?? "owner",
28
+ /** owner 字段为 NULL 时视为公开数据(查询自动追加 OR owner IS NULL),默认 false */
29
+ ownerNullOpen: (Bun.env.DB_AUTH_FIELD_NULL_OPEN ?? "false") === "true",
30
+ /** JWT 密钥 */
31
+ jwtSecret: Bun.env.AUTH_JWT_SECRET ?? "restbase",
32
+ /** JWT 过期秒数(默认 12 小时) */
33
+ jwtExp: Number(Bun.env.AUTH_JWT_EXP) || 43200,
34
+ /** 是否开启 Basic Auth(默认 true) */
35
+ basicAuth: (Bun.env.AUTH_BASIC_OPEN ?? "true") !== "false",
36
+ /** 日志等级 */
37
+ logLevel: (Bun.env.LOG_LEVEL ?? "INFO").toUpperCase() as "ERROR" | "INFO" | "DEBUG",
38
+ /** 是否输出到控制台(默认 true) */
39
+ logConsole: (Bun.env.LOG_CONSOLE ?? "true") !== "false",
40
+ /** 日志文件路径(不配置则不写文件) */
41
+ logFile: Bun.env.LOG_FILE ?? "",
42
+ /** 日志文件保留天数(默认 7) */
43
+ logRetainDays: Number(Bun.env.LOG_RETAIN_DAYS) || 7,
44
+ /** 初始化 SQL 文件路径(相对路径,启动时执行) */
45
+ initSql: Bun.env.DB_INIT_SQL ?? "",
46
+ };
47
+
48
+ /**
49
+ * 生成 owner 过滤条件 SQL 片段
50
+ * - 默认: `"owner" = $N`
51
+ * - ownerNullOpen=true: `("owner" = $N OR "owner" IS NULL)`
52
+ */
53
+ export function ownerCond(paramIdx: number | string): string {
54
+ const ph = typeof paramIdx === "string" ? paramIdx : `$${paramIdx}`;
55
+ const base = `${q(cfg.ownerField)} = ${ph}`;
56
+ if (cfg.ownerNullOpen) {
57
+ return `(${base} OR ${q(cfg.ownerField)} IS NULL)`;
58
+ }
59
+ return base;
60
+ }
61
+
62
+ /* ═══════════ 请求上下文(AsyncLocalStorage) ═══════════ */
63
+
64
+ export const reqStore = new AsyncLocalStorage<{ requestId: string }>();
65
+
66
+ /* ═══════════ SQL 标识符引用 ═══════════ */
67
+
68
+ /** SQLite 用双引号,MySQL 用反引号 */
69
+ export const q = (id: string) => (cfg.isSqlite ? `"${id}"` : `\`${id}\``);
70
+
71
+ /* ═══════════ Hono 环境泛型 ═══════════ */
72
+
73
+ export type AppEnv = { Variables: { userId: number; username: string; requestId: string } };
74
+
75
+ /* ═══════════ 统一 JSON 响应结构 ═══════════ */
76
+
77
+ export interface ApiRes {
78
+ code: string;
79
+ message?: string;
80
+ data?: unknown;
81
+ pageNo?: number;
82
+ pageSize?: number;
83
+ total?: number;
84
+ }
85
+
86
+ export const ok = (data?: unknown): ApiRes => ({code: "OK", data: data ?? null});
87
+
88
+ export const paged = (
89
+ rows: unknown[],
90
+ pageNo: number,
91
+ pageSize: number,
92
+ total: number,
93
+ ): ApiRes => ({code: "OK", data: rows, pageNo, pageSize, total});
94
+
95
+ /* ═══════════ 业务错误(HTTP 始终返回 200) ═══════════ */
96
+
97
+ export class AppError extends Error {
98
+ code: string;
99
+
100
+ constructor(code: string, message: string) {
101
+ super(message);
102
+ this.name = "AppError";
103
+ this.code = code;
104
+ }
105
+ }
106
+
107
+ /* ═══════════ Zod 校验 hook(失败时返回 200 + 统一格式) ═══════════ */
108
+
109
+ export const zodHook = (result: any, _c: any) => {
110
+ if (!result.success) {
111
+ const msg = result.error.issues.map((i: any) => i.message).join("; ");
112
+ return new Response(
113
+ JSON.stringify({code: "VALIDATION_ERROR", message: msg}, null, 2),
114
+ {status: 200, headers: {"Content-Type": "application/json"}},
115
+ );
116
+ }
117
+ };
118
+
119
+ /* ═══════════ Zod Schemas ═══════════ */
120
+
121
+ export const authBodySchema = z.object({
122
+ username: z.string().min(1, "username is required"),
123
+ password: z.string().min(1, "password is required"),
124
+ });
125
+
126
+ /* ── Body 输入类型(递归部分需手动声明类型) ── */
127
+
128
+ /** where 条件单元 */
129
+ export type BodyWhereItem =
130
+ | [string, unknown] // [field, value] 默认 eq
131
+ | [string, string, unknown] // [field, op, value]
132
+ | { field: string; op: string; value?: unknown } // 对象格式
133
+ | { op: "and" | "or"; cond: BodyWhereInput[] }; // 逻辑组合
134
+
135
+ /** where 顶层:单条件 或 条件数组 */
136
+ export type BodyWhereInput = BodyWhereItem | BodyWhereItem[];
137
+
138
+ export const bodyWhereItemSchema: z.ZodType<BodyWhereItem> = z.lazy(() =>
139
+ z.union([
140
+ z.tuple([z.string(), z.unknown()]),
141
+ z.tuple([z.string(), z.string(), z.unknown()]),
142
+ z.object({ field: z.string(), op: z.string(), value: z.unknown().optional() }),
143
+ z.object({ op: z.enum(["and", "or"]), cond: z.array(bodyWhereInputSchema) }),
144
+ ]),
145
+ );
146
+
147
+ export const bodyWhereInputSchema: z.ZodType<BodyWhereInput> = z.lazy(() =>
148
+ z.union([
149
+ bodyWhereItemSchema,
150
+ z.array(bodyWhereItemSchema),
151
+ ]),
152
+ );
153
+
154
+ /** select 项 */
155
+ export const bodySelectItemSchema = z.union([
156
+ z.string().min(1),
157
+ z.object({ field: z.string().min(1), alias: z.string().optional(), func: z.string().optional() }),
158
+ ]);
159
+ export type BodySelectItem = z.infer<typeof bodySelectItemSchema>;
160
+
161
+ /** order 项 */
162
+ export const bodyOrderItemSchema = z.union([
163
+ z.string().min(1),
164
+ z.object({ field: z.string().min(1), dir: z.enum(["asc", "desc"]).optional() }),
165
+ ]);
166
+ export type BodyOrderItem = z.infer<typeof bodyOrderItemSchema>;
167
+
168
+ /** POST /api/query/:table — 完整查询 Body */
169
+ export const bodyQuerySchema = z.object({
170
+ select: z.array(bodySelectItemSchema).optional(),
171
+ where: bodyWhereInputSchema.optional(),
172
+ order: z.array(bodyOrderItemSchema).optional(),
173
+ group: z.array(z.string()).optional(),
174
+ pageNo: z.number().int().min(1).optional(),
175
+ pageSize: z.number().int().min(1).max(1000).optional(),
176
+ });
177
+ export type BodyQuery = z.infer<typeof bodyQuerySchema>;
178
+
179
+ /** POST /api/delete/:table — 删除 Body(直接是 where 条件) */
180
+ export const bodyDeleteSchema = bodyWhereInputSchema;
181
+
182
+ /** POST|PUT /api/data/:table — 创建/更新 Body(单对象或对象数组) */
183
+ export const bodyDataSchema = z.union([
184
+ z.record(z.string(), z.unknown()),
185
+ z.array(z.record(z.string(), z.unknown())).min(1),
186
+ ]);