befly 3.14.3 → 3.15.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/dist/befly.config.js +1 -1
- package/dist/befly.js +618 -209
- package/dist/befly.min.js +13 -13
- package/dist/checks/checkApi.d.ts +1 -1
- package/dist/checks/checkApi.js +64 -24
- package/dist/checks/checkHook.js +20 -14
- package/dist/checks/checkPlugin.js +20 -14
- package/dist/checks/checkTable.js +6 -3
- package/dist/hooks/permission.js +1 -1
- package/dist/lib/cipher.js +3 -4
- package/dist/lib/connect.js +2 -1
- package/dist/lib/dbDialect.d.ts +4 -4
- package/dist/lib/dbDialect.js +3 -12
- package/dist/lib/dbHelper.d.ts +8 -3
- package/dist/lib/dbHelper.js +85 -35
- package/dist/lib/dbUtils.js +2 -2
- package/dist/lib/logger.d.ts +6 -11
- package/dist/lib/logger.js +15 -10
- package/dist/lib/sqlBuilder.js +10 -2
- package/dist/lib/validator.d.ts +3 -3
- package/dist/lib/validator.js +59 -7
- package/dist/loader/loadApis.js +38 -6
- package/dist/loader/loadHooks.js +12 -5
- package/dist/loader/loadPlugins.js +13 -6
- package/dist/plugins/tool.d.ts +11 -9
- package/dist/plugins/tool.js +1 -1
- package/dist/sync/syncApi.d.ts +2 -2
- package/dist/sync/syncApi.js +23 -11
- package/dist/sync/syncDev.js +2 -2
- package/dist/sync/syncMenu.js +3 -2
- package/dist/sync/syncTable.d.ts +22 -26
- package/dist/sync/syncTable.js +114 -25
- package/dist/types/api.d.ts +11 -10
- package/dist/types/befly.d.ts +12 -12
- package/dist/types/common.d.ts +23 -6
- package/dist/types/context.d.ts +7 -3
- package/dist/types/database.d.ts +15 -15
- package/dist/types/jwt.d.ts +3 -2
- package/dist/types/logger.d.ts +24 -7
- package/dist/types/sync.d.ts +7 -7
- package/dist/types/validate.d.ts +12 -4
- package/dist/utils/convertBigIntFields.d.ts +2 -1
- package/dist/utils/convertBigIntFields.js +3 -12
- package/dist/utils/isDirentDirectory.d.ts +2 -1
- package/dist/utils/loadMenuConfigs.d.ts +1 -1
- package/dist/utils/loadMenuConfigs.js +11 -3
- package/dist/utils/mergeAndConcat.d.ts +1 -1
- package/dist/utils/mergeAndConcat.js +22 -17
- package/dist/utils/normalizeFieldDefinition.d.ts +2 -1
- package/dist/utils/scanCoreBuiltins.js +9 -5
- package/dist/utils/scanFiles.d.ts +2 -2
- package/dist/utils/scanFiles.js +1 -1
- package/dist/utils/sortModules.js +8 -1
- package/dist/utils/sqlParams.d.ts +10 -0
- package/dist/utils/sqlParams.js +78 -0
- package/dist/utils/sqlResult.d.ts +5 -0
- package/dist/utils/sqlResult.js +7 -0
- package/dist/utils/util.d.ts +7 -6
- package/dist/utils/util.js +8 -5
- package/package.json +2 -2
|
@@ -7,13 +7,14 @@ import { sortModules } from "../utils/sortModules";
|
|
|
7
7
|
export async function loadPlugins(plugins, context) {
|
|
8
8
|
const pluginsMap = [];
|
|
9
9
|
const enabledPlugins = plugins.filter((item) => {
|
|
10
|
-
const moduleName = item
|
|
10
|
+
const moduleName = item.moduleName;
|
|
11
11
|
if (typeof moduleName !== "string" || moduleName.trim() === "") {
|
|
12
12
|
return false;
|
|
13
13
|
}
|
|
14
14
|
// enable=false 表示禁用(替代 disablePlugins 列表)。
|
|
15
15
|
// enable 仅允许 boolean;缺失 enable 的默认值应在 checkPlugin 阶段被补全为 true。
|
|
16
|
-
|
|
16
|
+
const enable = Object.hasOwn(item, "enable") ? item.enable : undefined;
|
|
17
|
+
if (enable === false) {
|
|
17
18
|
return false;
|
|
18
19
|
}
|
|
19
20
|
return true;
|
|
@@ -24,15 +25,21 @@ export async function loadPlugins(plugins, context) {
|
|
|
24
25
|
}
|
|
25
26
|
for (const item of sortedPlugins) {
|
|
26
27
|
const pluginName = item.moduleName;
|
|
27
|
-
const
|
|
28
|
+
const depsRaw = Object.hasOwn(item, "deps") ? item.deps : undefined;
|
|
29
|
+
const deps = Array.isArray(depsRaw) ? depsRaw.filter((x) => typeof x === "string") : [];
|
|
30
|
+
const handlerRaw = Object.hasOwn(item, "handler") ? item.handler : undefined;
|
|
31
|
+
if (typeof handlerRaw !== "function") {
|
|
32
|
+
throw new Error(`插件 '${pluginName}' handler 必须是函数`);
|
|
33
|
+
}
|
|
34
|
+
const handler = handlerRaw;
|
|
28
35
|
try {
|
|
29
|
-
const pluginInstance =
|
|
36
|
+
const pluginInstance = await handler(context);
|
|
30
37
|
context[pluginName] = pluginInstance;
|
|
31
38
|
pluginsMap.push({
|
|
32
39
|
name: pluginName,
|
|
33
40
|
enable: true,
|
|
34
|
-
deps:
|
|
35
|
-
handler:
|
|
41
|
+
deps: deps,
|
|
42
|
+
handler: handler
|
|
36
43
|
});
|
|
37
44
|
}
|
|
38
45
|
catch (error) {
|
package/dist/plugins/tool.d.ts
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
* 工具插件
|
|
3
3
|
* 提供常用的工具函数
|
|
4
4
|
*/
|
|
5
|
+
import type { JsonValue } from "../types/common";
|
|
5
6
|
import type { RequestContext } from "../types/context";
|
|
6
7
|
import type { Plugin } from "../types/plugin";
|
|
8
|
+
type ToolExtra = Record<string, JsonValue | undefined>;
|
|
7
9
|
/**
|
|
8
10
|
* 成功响应
|
|
9
11
|
* @param msg - 消息
|
|
@@ -11,11 +13,11 @@ import type { Plugin } from "../types/plugin";
|
|
|
11
13
|
* @param other - 其他字段(可选)
|
|
12
14
|
* @returns 成功响应对象
|
|
13
15
|
*/
|
|
14
|
-
export declare function Yes(msg: string, data?:
|
|
15
|
-
code:
|
|
16
|
+
export declare function Yes<TData extends JsonValue = JsonValue, TOther extends ToolExtra = ToolExtra>(msg: string, data?: TData, other?: TOther): {
|
|
17
|
+
code: 0;
|
|
16
18
|
msg: string;
|
|
17
|
-
data:
|
|
18
|
-
};
|
|
19
|
+
data: TData;
|
|
20
|
+
} & TOther;
|
|
19
21
|
/**
|
|
20
22
|
* 失败响应
|
|
21
23
|
* @param msg - 消息
|
|
@@ -23,11 +25,11 @@ export declare function Yes(msg: string, data?: any, other?: Record<string, any>
|
|
|
23
25
|
* @param other - 其他字段(可选)
|
|
24
26
|
* @returns 失败响应对象
|
|
25
27
|
*/
|
|
26
|
-
export declare function No(msg: string, data?:
|
|
27
|
-
code:
|
|
28
|
+
export declare function No<TData extends JsonValue = JsonValue, TOther extends ToolExtra = ToolExtra>(msg: string, data?: TData, other?: TOther): {
|
|
29
|
+
code: 1;
|
|
28
30
|
msg: string;
|
|
29
|
-
data:
|
|
30
|
-
};
|
|
31
|
+
data: TData;
|
|
32
|
+
} & TOther;
|
|
31
33
|
/**
|
|
32
34
|
* 响应选项
|
|
33
35
|
*/
|
|
@@ -68,6 +70,6 @@ interface ResponseOptions {
|
|
|
68
70
|
* headers: { 'X-Custom': 'value' }
|
|
69
71
|
* });
|
|
70
72
|
*/
|
|
71
|
-
export declare function Raw(ctx: RequestContext, data:
|
|
73
|
+
export declare function Raw(ctx: RequestContext, data: JsonValue | string, options?: ResponseOptions): Response;
|
|
72
74
|
declare const toolPlugin: Plugin;
|
|
73
75
|
export default toolPlugin;
|
package/dist/plugins/tool.js
CHANGED
package/dist/sync/syncApi.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { BeflyContext } from "../types/befly";
|
|
2
|
-
import type {
|
|
3
|
-
export declare function syncApi(ctx: Pick<BeflyContext, "db" | "cache">, apis:
|
|
2
|
+
import type { ScanFileResult } from "../utils/scanFiles";
|
|
3
|
+
export declare function syncApi(ctx: Pick<BeflyContext, "db" | "cache">, apis: ScanFileResult[]): Promise<void>;
|
package/dist/sync/syncApi.js
CHANGED
|
@@ -45,31 +45,43 @@ export async function syncApi(ctx, apis) {
|
|
|
45
45
|
if (api.type !== "api") {
|
|
46
46
|
continue;
|
|
47
47
|
}
|
|
48
|
+
const record = api;
|
|
49
|
+
const path = record["path"];
|
|
50
|
+
const name = record["name"];
|
|
51
|
+
const addonNameRaw = record["addonName"];
|
|
52
|
+
if (typeof path !== "string" || path.trim() === "") {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (typeof name !== "string" || name.trim() === "") {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const addonName = typeof addonNameRaw === "string" ? addonNameRaw : "";
|
|
48
59
|
// auth:运行时 API 定义使用 boolean;DB 字段使用 0/1。
|
|
49
60
|
// 统一在 syncApi 写库前做归一化,避免类型不一致导致每次启动都触发更新。
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
61
|
+
const authRaw = record["auth"];
|
|
62
|
+
const auth = authRaw === false || authRaw === 0 ? 0 : 1;
|
|
63
|
+
const parentPath = getApiParentPath(path);
|
|
64
|
+
apiRouteKeys.add(path);
|
|
65
|
+
const item = allDbApiMap[path];
|
|
54
66
|
if (item) {
|
|
55
|
-
const shouldUpdate =
|
|
67
|
+
const shouldUpdate = name !== item.name || path !== item.path || addonName !== item.addonName || parentPath !== item.parentPath || auth !== item.auth;
|
|
56
68
|
if (shouldUpdate) {
|
|
57
69
|
updData.push({
|
|
58
70
|
id: item.id,
|
|
59
|
-
name:
|
|
60
|
-
path:
|
|
71
|
+
name: name,
|
|
72
|
+
path: path,
|
|
61
73
|
parentPath: parentPath,
|
|
62
|
-
addonName:
|
|
74
|
+
addonName: addonName,
|
|
63
75
|
auth: auth
|
|
64
76
|
});
|
|
65
77
|
}
|
|
66
78
|
}
|
|
67
79
|
else {
|
|
68
80
|
insData.push({
|
|
69
|
-
name:
|
|
70
|
-
path:
|
|
81
|
+
name: name,
|
|
82
|
+
path: path,
|
|
71
83
|
parentPath: parentPath,
|
|
72
|
-
addonName:
|
|
84
|
+
addonName: addonName,
|
|
73
85
|
auth: auth
|
|
74
86
|
});
|
|
75
87
|
}
|
package/dist/sync/syncDev.js
CHANGED
|
@@ -46,8 +46,8 @@ export async function syncDev(ctx, config = {}) {
|
|
|
46
46
|
code: "dev",
|
|
47
47
|
name: "开发者角色",
|
|
48
48
|
description: "拥有所有菜单和接口权限的开发者角色",
|
|
49
|
-
menus: allMenus.data.lists.map((item) => item.path).filter((v) => v),
|
|
50
|
-
apis: allApis.data.lists.map((item) => item.path).filter((v) => v),
|
|
49
|
+
menus: allMenus.data.lists.map((item) => item.path).filter((v) => typeof v === "string" && v.length > 0),
|
|
50
|
+
apis: allApis.data.lists.map((item) => item.path).filter((v) => typeof v === "string" && v.length > 0),
|
|
51
51
|
sort: 0
|
|
52
52
|
};
|
|
53
53
|
if (typeof devRole.data.id === "number") {
|
package/dist/sync/syncMenu.js
CHANGED
|
@@ -36,12 +36,13 @@ function createDisableMenuMatcher(ctx) {
|
|
|
36
36
|
candidates.push(`/${trimmed}`);
|
|
37
37
|
}
|
|
38
38
|
for (const glob of globs) {
|
|
39
|
-
const match = glob.match;
|
|
39
|
+
const match = typeof glob === "object" && glob !== null && "match" in glob ? glob.match : undefined;
|
|
40
40
|
if (typeof match !== "function") {
|
|
41
41
|
throw new Error("syncMenu: 当前 Bun 版本不支持 Bun.Glob.match,无法按 disableMenus 做 glob 匹配");
|
|
42
42
|
}
|
|
43
|
+
const matchFn = match;
|
|
43
44
|
for (const candidate of candidates) {
|
|
44
|
-
if (
|
|
45
|
+
if (matchFn.call(glob, candidate)) {
|
|
45
46
|
return true;
|
|
46
47
|
}
|
|
47
48
|
}
|
package/dist/sync/syncTable.d.ts
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
* - 现在按项目要求,将所有实现合并到本文件(目录 packages/core/sync/syncTable/ 已删除)
|
|
7
7
|
*/
|
|
8
8
|
import type { DbDialectName } from "../lib/dbDialect";
|
|
9
|
-
import type { BeflyContext } from "../types/befly";
|
|
10
9
|
import type { DbResult, SqlInfo } from "../types/database";
|
|
11
10
|
import type { ColumnInfo, FieldChange, IndexInfo } from "../types/sync";
|
|
12
11
|
import type { FieldDefinition } from "../types/validate";
|
|
@@ -14,6 +13,18 @@ import type { ScanFileResult } from "../utils/scanFiles";
|
|
|
14
13
|
type SqlExecutor = {
|
|
15
14
|
unsafe<T = unknown>(sqlStr: string, params?: unknown[]): Promise<DbResult<T, SqlInfo>>;
|
|
16
15
|
};
|
|
16
|
+
type SyncTableContext = {
|
|
17
|
+
db: SqlExecutor;
|
|
18
|
+
redis: {
|
|
19
|
+
delBatch(keys: string[]): Promise<unknown>;
|
|
20
|
+
};
|
|
21
|
+
config: {
|
|
22
|
+
db?: {
|
|
23
|
+
type?: string;
|
|
24
|
+
database?: string;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
};
|
|
17
28
|
type DbDialect = DbDialectName;
|
|
18
29
|
/**
|
|
19
30
|
* 文件导航(推荐阅读顺序)
|
|
@@ -23,7 +34,7 @@ type DbDialect = DbDialectName;
|
|
|
23
34
|
* 4) Runtime I/O(只读元信息:表/列/索引/版本)
|
|
24
35
|
* 5) plan/apply(写变更:建表/改表/SQLite 重建)
|
|
25
36
|
*/
|
|
26
|
-
type SyncTableFn = ((ctx:
|
|
37
|
+
type SyncTableFn = ((ctx: SyncTableContext, items: ScanFileResult[]) => Promise<void>) & {
|
|
27
38
|
TestKit: typeof SYNC_TABLE_TEST_KIT;
|
|
28
39
|
};
|
|
29
40
|
/**
|
|
@@ -68,7 +79,7 @@ declare const SYNC_TABLE_TEST_KIT: {
|
|
|
68
79
|
tableExistsRuntime: typeof tableExistsRuntime;
|
|
69
80
|
getTableColumnsRuntime: typeof getTableColumnsRuntime;
|
|
70
81
|
getTableIndexesRuntime: typeof getTableIndexesRuntime;
|
|
71
|
-
createRuntime: (dbDialect: DbDialectName, db: SqlExecutor, dbName?: string) =>
|
|
82
|
+
createRuntime: (dbDialect: DbDialectName, db: SqlExecutor | null, dbName?: string) => SyncRuntimeForIO;
|
|
72
83
|
};
|
|
73
84
|
/**
|
|
74
85
|
* 获取字段类型映射(根据当前数据库类型)
|
|
@@ -82,10 +93,7 @@ declare function quoteIdentifier(dbDialect: DbDialect, identifier: string): stri
|
|
|
82
93
|
* 转义 SQL 注释中的双引号
|
|
83
94
|
*/
|
|
84
95
|
declare function escapeComment(str: string): string;
|
|
85
|
-
|
|
86
|
-
* 为字段定义应用默认值
|
|
87
|
-
*/
|
|
88
|
-
declare function applyFieldDefaults(fieldDef: any): void;
|
|
96
|
+
declare function applyFieldDefaults(fieldDef: unknown): void;
|
|
89
97
|
/**
|
|
90
98
|
* 判断是否为字符串或数组类型(需要长度参数)
|
|
91
99
|
*/
|
|
@@ -121,31 +129,19 @@ declare function generateDDLClause(dbDialect: DbDialect, fieldKey: string, field
|
|
|
121
129
|
/**
|
|
122
130
|
* 判断是否为兼容的类型变更(宽化型变更,无数据丢失风险)
|
|
123
131
|
*/
|
|
124
|
-
declare function isCompatibleTypeChange(currentType: string, newType: string): boolean;
|
|
125
|
-
type
|
|
126
|
-
/**
|
|
127
|
-
* 当前数据库方言(mysql/postgresql/sqlite),决定 SQL 片段与元信息查询方式。
|
|
128
|
-
* 约束:必须与 ctx.config.db.type 一致(经归一化)。
|
|
129
|
-
*/
|
|
132
|
+
declare function isCompatibleTypeChange(currentType: string | null | undefined, newType: string | null | undefined): boolean;
|
|
133
|
+
type SyncRuntimeForIO = {
|
|
130
134
|
dbDialect: DbDialect;
|
|
131
|
-
|
|
132
|
-
* SQL 执行器:必须复用 ctx.db。
|
|
133
|
-
* 约束:syncTable 内部禁止新建 DB 连接/事务;runtime 仅保存引用,不拥有生命周期。
|
|
134
|
-
*/
|
|
135
|
-
db: SqlExecutor;
|
|
136
|
-
/**
|
|
137
|
-
* 数据库名:主要用于 MySQL information_schema 查询。
|
|
138
|
-
* 约束:PG/SQLite 可以传空字符串;不要在非 MySQL 方言依赖该值。
|
|
139
|
-
*/
|
|
135
|
+
db: SqlExecutor | null;
|
|
140
136
|
dbName: string;
|
|
141
137
|
};
|
|
142
|
-
declare function tableExistsRuntime(runtime:
|
|
143
|
-
declare function getTableColumnsRuntime(runtime:
|
|
138
|
+
declare function tableExistsRuntime(runtime: SyncRuntimeForIO, tableName: string): Promise<boolean>;
|
|
139
|
+
declare function getTableColumnsRuntime(runtime: SyncRuntimeForIO, tableName: string): Promise<{
|
|
144
140
|
[key: string]: ColumnInfo;
|
|
145
141
|
}>;
|
|
146
|
-
declare function getTableIndexesRuntime(runtime:
|
|
142
|
+
declare function getTableIndexesRuntime(runtime: SyncRuntimeForIO, tableName: string): Promise<IndexInfo>;
|
|
147
143
|
/**
|
|
148
144
|
* 比较字段定义变化
|
|
149
145
|
*/
|
|
150
|
-
declare function compareFieldDefinition(dbDialect: DbDialect, existingColumn: ColumnInfo, fieldDef: FieldDefinition): FieldChange[];
|
|
146
|
+
declare function compareFieldDefinition(dbDialect: DbDialect, existingColumn: Pick<ColumnInfo, "type" | "max" | "nullable" | "defaultValue" | "comment">, fieldDef: FieldDefinition): FieldChange[];
|
|
151
147
|
export {};
|
package/dist/sync/syncTable.js
CHANGED
|
@@ -89,11 +89,12 @@ export const syncTable = (async (ctx, items) => {
|
|
|
89
89
|
applyFieldDefaults(fieldDef);
|
|
90
90
|
}
|
|
91
91
|
const existsTable = await tableExistsRuntime(runtime, tableName);
|
|
92
|
+
const tableFields = tableDefinition;
|
|
92
93
|
if (existsTable) {
|
|
93
|
-
await modifyTableRuntime(runtime, tableName,
|
|
94
|
+
await modifyTableRuntime(runtime, tableName, tableFields);
|
|
94
95
|
}
|
|
95
96
|
else {
|
|
96
|
-
await createTable(runtime, tableName,
|
|
97
|
+
await createTable(runtime, tableName, tableFields);
|
|
97
98
|
}
|
|
98
99
|
// 记录处理过的表名(用于清理缓存)
|
|
99
100
|
processedTables.push(tableName);
|
|
@@ -264,24 +265,105 @@ function quoteIdentifier(dbDialect, identifier) {
|
|
|
264
265
|
function escapeComment(str) {
|
|
265
266
|
return String(str).replace(/"/g, '\\"');
|
|
266
267
|
}
|
|
268
|
+
function normalizeColumnDefaultValue(value) {
|
|
269
|
+
if (value === null)
|
|
270
|
+
return null;
|
|
271
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean")
|
|
272
|
+
return value;
|
|
273
|
+
if (Array.isArray(value)) {
|
|
274
|
+
const items = [];
|
|
275
|
+
for (const v of value) {
|
|
276
|
+
items.push(normalizeColumnDefaultValue(v));
|
|
277
|
+
}
|
|
278
|
+
return items;
|
|
279
|
+
}
|
|
280
|
+
return String(value);
|
|
281
|
+
}
|
|
267
282
|
// 注意:这里刻意不封装“logFieldChange/formatFieldList”之类的一次性工具函数,
|
|
268
283
|
// 以减少抽象层级(按项目要求:能直写就直写)。
|
|
269
284
|
/**
|
|
270
285
|
* 为字段定义应用默认值
|
|
271
286
|
*/
|
|
287
|
+
function isJsonValue(value) {
|
|
288
|
+
if (value === null)
|
|
289
|
+
return true;
|
|
290
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean")
|
|
291
|
+
return true;
|
|
292
|
+
if (Array.isArray(value)) {
|
|
293
|
+
return value.every((v) => isJsonValue(v));
|
|
294
|
+
}
|
|
295
|
+
if (typeof value === "object") {
|
|
296
|
+
for (const v of Object.values(value)) {
|
|
297
|
+
if (v === undefined)
|
|
298
|
+
continue;
|
|
299
|
+
if (!isJsonValue(v))
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
272
306
|
function applyFieldDefaults(fieldDef) {
|
|
273
307
|
if (!fieldDef || typeof fieldDef !== "object")
|
|
274
308
|
return;
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
309
|
+
const record = fieldDef;
|
|
310
|
+
const name = record["name"];
|
|
311
|
+
const type = record["type"];
|
|
312
|
+
if (typeof name !== "string" || typeof type !== "string")
|
|
313
|
+
return;
|
|
314
|
+
const minRaw = record["min"];
|
|
315
|
+
const maxRaw = record["max"];
|
|
316
|
+
const defaultRaw = record["default"];
|
|
317
|
+
const detailRaw = record["detail"];
|
|
318
|
+
const indexRaw = record["index"];
|
|
319
|
+
const uniqueRaw = record["unique"];
|
|
320
|
+
const nullableRaw = record["nullable"];
|
|
321
|
+
const unsignedRaw = record["unsigned"];
|
|
322
|
+
const regexpRaw = record["regexp"];
|
|
323
|
+
const input = {
|
|
324
|
+
name: name,
|
|
325
|
+
type: type
|
|
326
|
+
};
|
|
327
|
+
if (typeof detailRaw === "string") {
|
|
328
|
+
input.detail = detailRaw;
|
|
329
|
+
}
|
|
330
|
+
if (typeof minRaw === "number" || minRaw === null) {
|
|
331
|
+
input.min = minRaw;
|
|
332
|
+
}
|
|
333
|
+
if (typeof maxRaw === "number" || maxRaw === null) {
|
|
334
|
+
input.max = maxRaw;
|
|
335
|
+
}
|
|
336
|
+
if (defaultRaw === null) {
|
|
337
|
+
input.default = null;
|
|
338
|
+
}
|
|
339
|
+
else if (isJsonValue(defaultRaw)) {
|
|
340
|
+
input.default = defaultRaw;
|
|
341
|
+
}
|
|
342
|
+
if (typeof indexRaw === "boolean") {
|
|
343
|
+
input.index = indexRaw;
|
|
344
|
+
}
|
|
345
|
+
if (typeof uniqueRaw === "boolean") {
|
|
346
|
+
input.unique = uniqueRaw;
|
|
347
|
+
}
|
|
348
|
+
if (typeof nullableRaw === "boolean") {
|
|
349
|
+
input.nullable = nullableRaw;
|
|
350
|
+
}
|
|
351
|
+
if (typeof unsignedRaw === "boolean") {
|
|
352
|
+
input.unsigned = unsignedRaw;
|
|
353
|
+
}
|
|
354
|
+
if (typeof regexpRaw === "string" || regexpRaw === null) {
|
|
355
|
+
input.regexp = regexpRaw;
|
|
356
|
+
}
|
|
357
|
+
const normalized = normalizeFieldDefinition(input);
|
|
358
|
+
record["detail"] = normalized.detail;
|
|
359
|
+
record["min"] = normalized.min;
|
|
360
|
+
record["max"] = normalized.max;
|
|
361
|
+
record["default"] = normalized.default;
|
|
362
|
+
record["index"] = normalized.index;
|
|
363
|
+
record["unique"] = normalized.unique;
|
|
364
|
+
record["nullable"] = normalized.nullable;
|
|
365
|
+
record["unsigned"] = normalized.unsigned;
|
|
366
|
+
record["regexp"] = normalized.regexp;
|
|
285
367
|
}
|
|
286
368
|
/**
|
|
287
369
|
* 判断是否为字符串或数组类型(需要长度参数)
|
|
@@ -538,7 +620,8 @@ function isCompatibleTypeChange(currentType, newType) {
|
|
|
538
620
|
// 读:表是否存在
|
|
539
621
|
// ---------------------------------------------------------------------------
|
|
540
622
|
async function tableExistsRuntime(runtime, tableName) {
|
|
541
|
-
|
|
623
|
+
const db = runtime.db;
|
|
624
|
+
if (!db)
|
|
542
625
|
throw new Error("SQL 执行器未初始化");
|
|
543
626
|
try {
|
|
544
627
|
// 统一交由方言层构造 SQL;syncTable 仅决定“要查哪个 schema/db”。
|
|
@@ -553,7 +636,7 @@ async function tableExistsRuntime(runtime, tableName) {
|
|
|
553
636
|
schema = "public";
|
|
554
637
|
}
|
|
555
638
|
const q = getDialectByName(runtime.dbDialect).tableExistsQuery(tableName, schema);
|
|
556
|
-
const res = await
|
|
639
|
+
const res = await db.unsafe(q.sql, q.params);
|
|
557
640
|
return (res.data?.[0]?.count || 0) > 0;
|
|
558
641
|
}
|
|
559
642
|
catch (error) {
|
|
@@ -569,6 +652,9 @@ async function tableExistsRuntime(runtime, tableName) {
|
|
|
569
652
|
// ---------------------------------------------------------------------------
|
|
570
653
|
async function getTableColumnsRuntime(runtime, tableName) {
|
|
571
654
|
const columns = {};
|
|
655
|
+
const db = runtime.db;
|
|
656
|
+
if (!db)
|
|
657
|
+
throw new Error("SQL 执行器未初始化");
|
|
572
658
|
try {
|
|
573
659
|
// 方言差异说明:
|
|
574
660
|
// - MySQL:information_schema.columns 最完整,包含 COLUMN_TYPE 与 COLUMN_COMMENT。
|
|
@@ -576,9 +662,9 @@ async function getTableColumnsRuntime(runtime, tableName) {
|
|
|
576
662
|
// - SQLite:PRAGMA table_info 仅提供 type/notnull/default 等有限信息,无列注释。
|
|
577
663
|
if (runtime.dbDialect === "mysql") {
|
|
578
664
|
const q = getSyncTableColumnsInfoQuery({ dialect: "mysql", table: tableName, dbName: runtime.dbName });
|
|
579
|
-
const result = await
|
|
665
|
+
const result = await db.unsafe(q.columns.sql, q.columns.params);
|
|
580
666
|
for (const row of result.data) {
|
|
581
|
-
const defaultValue = row.COLUMN_DEFAULT;
|
|
667
|
+
const defaultValue = normalizeColumnDefaultValue(row.COLUMN_DEFAULT);
|
|
582
668
|
columns[row.COLUMN_NAME] = {
|
|
583
669
|
type: row.DATA_TYPE,
|
|
584
670
|
columnType: row.COLUMN_TYPE,
|
|
@@ -592,8 +678,8 @@ async function getTableColumnsRuntime(runtime, tableName) {
|
|
|
592
678
|
}
|
|
593
679
|
else if (runtime.dbDialect === "postgresql") {
|
|
594
680
|
const q = getSyncTableColumnsInfoQuery({ dialect: "postgresql", table: tableName, dbName: runtime.dbName });
|
|
595
|
-
const result = await
|
|
596
|
-
const comments = q.comments ? (await
|
|
681
|
+
const result = await db.unsafe(q.columns.sql, q.columns.params);
|
|
682
|
+
const comments = q.comments ? (await db.unsafe(q.comments.sql, q.comments.params)).data : [];
|
|
597
683
|
const commentMap = {};
|
|
598
684
|
for (const r of comments)
|
|
599
685
|
commentMap[r.column_name] = r.column_comment;
|
|
@@ -604,14 +690,14 @@ async function getTableColumnsRuntime(runtime, tableName) {
|
|
|
604
690
|
length: row.character_maximum_length,
|
|
605
691
|
max: row.character_maximum_length,
|
|
606
692
|
nullable: String(row.is_nullable).toUpperCase() === "YES",
|
|
607
|
-
defaultValue: row.column_default,
|
|
693
|
+
defaultValue: normalizeColumnDefaultValue(row.column_default),
|
|
608
694
|
comment: commentMap[row.column_name] ?? null
|
|
609
695
|
};
|
|
610
696
|
}
|
|
611
697
|
}
|
|
612
698
|
else if (runtime.dbDialect === "sqlite") {
|
|
613
699
|
const q = getSyncTableColumnsInfoQuery({ dialect: "sqlite", table: tableName, dbName: runtime.dbName });
|
|
614
|
-
const result = await
|
|
700
|
+
const result = await db.unsafe(q.columns.sql, q.columns.params);
|
|
615
701
|
for (const row of result.data) {
|
|
616
702
|
let baseType = String(row.type || "").toUpperCase();
|
|
617
703
|
let max = null;
|
|
@@ -632,7 +718,7 @@ async function getTableColumnsRuntime(runtime, tableName) {
|
|
|
632
718
|
length: max,
|
|
633
719
|
max: max,
|
|
634
720
|
nullable: row.notnull === 0,
|
|
635
|
-
defaultValue: row.dflt_value,
|
|
721
|
+
defaultValue: normalizeColumnDefaultValue(row.dflt_value),
|
|
636
722
|
comment: null
|
|
637
723
|
};
|
|
638
724
|
}
|
|
@@ -652,6 +738,9 @@ async function getTableColumnsRuntime(runtime, tableName) {
|
|
|
652
738
|
// ---------------------------------------------------------------------------
|
|
653
739
|
async function getTableIndexesRuntime(runtime, tableName) {
|
|
654
740
|
const indexes = {};
|
|
741
|
+
const db = runtime.db;
|
|
742
|
+
if (!db)
|
|
743
|
+
throw new Error("SQL 执行器未初始化");
|
|
655
744
|
try {
|
|
656
745
|
// 方言差异说明:
|
|
657
746
|
// - MySQL:information_schema.statistics 直接给出 index -> column 映射。
|
|
@@ -659,7 +748,7 @@ async function getTableIndexesRuntime(runtime, tableName) {
|
|
|
659
748
|
// - SQLite:PRAGMA index_list + index_info;同样仅收集单列索引,避免多列索引误判。
|
|
660
749
|
if (runtime.dbDialect === "mysql") {
|
|
661
750
|
const q = getSyncTableIndexesQuery({ dialect: "mysql", table: tableName, dbName: runtime.dbName });
|
|
662
|
-
const result = await
|
|
751
|
+
const result = await db.unsafe(q.sql, q.params);
|
|
663
752
|
for (const row of result.data) {
|
|
664
753
|
const indexName = row.INDEX_NAME;
|
|
665
754
|
const current = indexes[indexName];
|
|
@@ -673,7 +762,7 @@ async function getTableIndexesRuntime(runtime, tableName) {
|
|
|
673
762
|
}
|
|
674
763
|
else if (runtime.dbDialect === "postgresql") {
|
|
675
764
|
const q = getSyncTableIndexesQuery({ dialect: "postgresql", table: tableName, dbName: runtime.dbName });
|
|
676
|
-
const result = await
|
|
765
|
+
const result = await db.unsafe(q.sql, q.params);
|
|
677
766
|
for (const row of result.data) {
|
|
678
767
|
const m = /\(([^)]+)\)/.exec(row.indexdef);
|
|
679
768
|
if (m) {
|
|
@@ -685,10 +774,10 @@ async function getTableIndexesRuntime(runtime, tableName) {
|
|
|
685
774
|
}
|
|
686
775
|
else if (runtime.dbDialect === "sqlite") {
|
|
687
776
|
const quotedTable = quoteIdentifier("sqlite", tableName);
|
|
688
|
-
const list = await
|
|
777
|
+
const list = await db.unsafe(`PRAGMA index_list(${quotedTable})`);
|
|
689
778
|
for (const idx of list.data) {
|
|
690
779
|
const quotedIndex = quoteIdentifier("sqlite", idx.name);
|
|
691
|
-
const info = await
|
|
780
|
+
const info = await db.unsafe(`PRAGMA index_info(${quotedIndex})`);
|
|
692
781
|
const cols = info.data.map((r) => r.name);
|
|
693
782
|
if (cols.length === 1)
|
|
694
783
|
indexes[idx.name] = cols;
|
package/dist/types/api.d.ts
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* Befly API 类型定义
|
|
3
3
|
*/
|
|
4
4
|
import type { BeflyContext } from "./befly";
|
|
5
|
-
import type { KeyValue } from "./common";
|
|
5
|
+
import type { JsonValue, KeyValue } from "./common";
|
|
6
6
|
import type { RequestContext } from "./context";
|
|
7
|
+
import type { JwtPayload } from "./jwt";
|
|
7
8
|
import type { TableDefinition } from "./validate";
|
|
8
9
|
/**
|
|
9
10
|
* HTTP 方法类型
|
|
@@ -18,7 +19,7 @@ export interface UserInfo {
|
|
|
18
19
|
username?: string;
|
|
19
20
|
email?: string;
|
|
20
21
|
role?: string;
|
|
21
|
-
[key: string]:
|
|
22
|
+
[key: string]: JsonValue | undefined;
|
|
22
23
|
}
|
|
23
24
|
/**
|
|
24
25
|
* 认证类型
|
|
@@ -32,7 +33,7 @@ export type AuthType = boolean | "admin" | "user" | string[];
|
|
|
32
33
|
/**
|
|
33
34
|
* API 处理器函数类型
|
|
34
35
|
*/
|
|
35
|
-
export type ApiHandler<TBody =
|
|
36
|
+
export type ApiHandler<TBody = Record<string, JsonValue>, R = JsonValue> = (befly: BeflyContext, ctx: RequestContext<TBody>) => Promise<Response | R> | Response | R;
|
|
36
37
|
/**
|
|
37
38
|
* 字段规则定义
|
|
38
39
|
* 键为字段名,值为字段规则字符串
|
|
@@ -41,7 +42,7 @@ export type FieldRules = Record<string, string>;
|
|
|
41
42
|
/**
|
|
42
43
|
* API 配置选项
|
|
43
44
|
*/
|
|
44
|
-
export interface ApiOptions<TBody =
|
|
45
|
+
export interface ApiOptions<TBody = Record<string, JsonValue>, R = JsonValue> {
|
|
45
46
|
/** HTTP 方法 */
|
|
46
47
|
method: HttpMethod;
|
|
47
48
|
/** 是否需要认证(true/false/角色数组) */
|
|
@@ -56,7 +57,7 @@ export interface ApiOptions<TBody = any, R = any> {
|
|
|
56
57
|
/**
|
|
57
58
|
* API 路由配置
|
|
58
59
|
*/
|
|
59
|
-
export interface ApiRoute<TBody =
|
|
60
|
+
export interface ApiRoute<TBody = Record<string, JsonValue>, R = JsonValue> {
|
|
60
61
|
/** 接口名称(必填) */
|
|
61
62
|
name: string;
|
|
62
63
|
/** 处理器函数(必填) */
|
|
@@ -97,7 +98,7 @@ export interface ApiBuilder {
|
|
|
97
98
|
/** 设置必填字段 */
|
|
98
99
|
required(fields: string[]): this;
|
|
99
100
|
/** 设置处理器 */
|
|
100
|
-
handler<TBody =
|
|
101
|
+
handler<TBody = Record<string, JsonValue>, R = JsonValue>(handler: ApiHandler<TBody, R>): this;
|
|
101
102
|
/** 构建路由 */
|
|
102
103
|
build(): ApiRoute;
|
|
103
104
|
}
|
|
@@ -106,11 +107,11 @@ export interface ApiBuilder {
|
|
|
106
107
|
*/
|
|
107
108
|
export interface ApiResponse {
|
|
108
109
|
/** 成功响应 */
|
|
109
|
-
success<T =
|
|
110
|
+
success<T = JsonValue>(message: string, data?: T): Response;
|
|
110
111
|
/** 失败响应 */
|
|
111
|
-
error(message: string, error?:
|
|
112
|
+
error(message: string, error?: Error | JsonValue): Response;
|
|
112
113
|
/** JSON 响应 */
|
|
113
|
-
json<T =
|
|
114
|
+
json<T = JsonValue>(data: T, status?: number): Response;
|
|
114
115
|
/** 文本响应 */
|
|
115
116
|
text(text: string, status?: number): Response;
|
|
116
117
|
/** HTML 响应 */
|
|
@@ -147,7 +148,7 @@ export interface TokenCheckData {
|
|
|
147
148
|
/** 令牌是否有效 */
|
|
148
149
|
valid: boolean;
|
|
149
150
|
/** JWT 载荷(有效时返回) */
|
|
150
|
-
payload?:
|
|
151
|
+
payload?: JwtPayload;
|
|
151
152
|
/** 过期时间(秒) */
|
|
152
153
|
expiresIn?: number;
|
|
153
154
|
}
|