befly 3.9.40 → 3.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -19
- package/befly.config.ts +19 -2
- package/checks/checkApi.ts +79 -77
- package/checks/checkHook.ts +48 -0
- package/checks/checkMenu.ts +168 -0
- package/checks/checkPlugin.ts +48 -0
- package/checks/checkTable.ts +137 -183
- package/docs/README.md +17 -11
- package/docs/api/api.md +16 -2
- package/docs/guide/quickstart.md +31 -10
- package/docs/hooks/hook.md +2 -2
- package/docs/hooks/rateLimit.md +1 -1
- package/docs/infra/redis.md +26 -14
- package/docs/plugins/plugin.md +23 -21
- package/docs/quickstart.md +5 -328
- package/docs/reference/addon.md +0 -4
- package/docs/reference/config.md +14 -31
- package/docs/reference/logger.md +3 -3
- package/docs/reference/sync.md +132 -237
- package/docs/reference/table.md +28 -30
- package/hooks/auth.ts +3 -4
- package/hooks/cors.ts +4 -6
- package/hooks/parser.ts +3 -4
- package/hooks/permission.ts +3 -4
- package/hooks/validator.ts +3 -4
- package/lib/cacheHelper.ts +89 -153
- package/lib/cacheKeys.ts +1 -1
- package/lib/connect.ts +9 -13
- package/lib/dbDialect.ts +285 -0
- package/lib/dbHelper.ts +179 -507
- package/lib/dbUtils.ts +450 -0
- package/lib/logger.ts +41 -5
- package/lib/redisHelper.ts +1 -0
- package/lib/sqlBuilder.ts +358 -58
- package/lib/sqlCheck.ts +136 -0
- package/lib/validator.ts +1 -1
- package/loader/loadApis.ts +23 -126
- package/loader/loadHooks.ts +31 -46
- package/loader/loadPlugins.ts +37 -52
- package/main.ts +58 -19
- package/package.json +24 -25
- package/paths.ts +14 -14
- package/plugins/cache.ts +12 -6
- package/plugins/cipher.ts +2 -2
- package/plugins/config.ts +6 -8
- package/plugins/db.ts +14 -19
- package/plugins/jwt.ts +6 -7
- package/plugins/logger.ts +7 -9
- package/plugins/redis.ts +8 -10
- package/plugins/tool.ts +3 -4
- package/router/api.ts +3 -2
- package/router/static.ts +7 -5
- package/sync/syncApi.ts +80 -235
- package/sync/syncCache.ts +16 -0
- package/sync/syncDev.ts +167 -202
- package/sync/syncMenu.ts +230 -444
- package/sync/syncTable.ts +1247 -0
- package/tests/_mocks/mockSqliteDb.ts +204 -0
- package/tests/addonHelper-cache.test.ts +32 -0
- package/tests/apiHandler-routePath-only.test.ts +32 -0
- package/tests/cacheHelper.test.ts +16 -51
- package/tests/checkApi-routePath-strict.test.ts +166 -0
- package/tests/checkMenu.test.ts +346 -0
- package/tests/checkTable-smoke.test.ts +157 -0
- package/tests/dbDialect-cache.test.ts +23 -0
- package/tests/dbDialect.test.ts +46 -0
- package/tests/dbHelper-advanced.test.ts +1 -1
- package/tests/dbHelper-all-array-types.test.ts +15 -15
- package/tests/dbHelper-batch-write.test.ts +90 -0
- package/tests/dbHelper-columns.test.ts +36 -54
- package/tests/dbHelper-execute.test.ts +26 -26
- package/tests/dbHelper-joins.test.ts +85 -176
- package/tests/fixtures/scanFilesAddon/node_modules/@befly-addon/demo/apis/sub/b.ts +3 -0
- package/tests/fixtures/scanFilesApis/a.ts +3 -0
- package/tests/fixtures/scanFilesApis/sub/b.ts +3 -0
- package/tests/loadPlugins-order-smoke.test.ts +75 -0
- package/tests/logger.test.ts +6 -6
- package/tests/redisHelper.test.ts +6 -1
- package/tests/scanFiles-routePath.test.ts +46 -0
- package/tests/smoke-sql.test.ts +24 -0
- package/tests/sqlBuilder-advanced.test.ts +18 -5
- package/tests/sqlBuilder.test.ts +24 -0
- package/tests/sync-init-guard.test.ts +105 -0
- package/tests/syncApi-insBatch-fields-consistent.test.ts +61 -0
- package/tests/syncApi-obsolete-records.test.ts +69 -0
- package/tests/syncApi-type-compat.test.ts +72 -0
- package/tests/syncDev-permissions.test.ts +81 -0
- package/tests/syncMenu-disableMenus-hard-delete.test.ts +88 -0
- package/tests/syncMenu-duplicate-path.test.ts +122 -0
- package/tests/syncMenu-obsolete-records.test.ts +161 -0
- package/tests/syncMenu-parentPath-from-tree.test.ts +75 -0
- package/tests/syncMenu-paths.test.ts +0 -9
- package/tests/{syncDb-apply.test.ts → syncTable-apply.test.ts} +14 -24
- package/tests/{syncDb-array-number.test.ts → syncTable-array-number.test.ts} +31 -31
- package/tests/syncTable-constants.test.ts +101 -0
- package/tests/syncTable-db-integration.test.ts +237 -0
- package/tests/{syncDb-ddl.test.ts → syncTable-ddl.test.ts} +67 -53
- package/tests/{syncDb-helpers.test.ts → syncTable-helpers.test.ts} +12 -26
- package/tests/syncTable-schema.test.ts +99 -0
- package/tests/syncTable-testkit.test.ts +25 -0
- package/tests/syncTable-types.test.ts +122 -0
- package/tests/tableRef-and-deserialize.test.ts +67 -0
- package/tsconfig.json +1 -1
- package/types/api.d.ts +1 -1
- package/types/befly.d.ts +13 -12
- package/types/cache.d.ts +2 -2
- package/types/context.d.ts +1 -1
- package/types/database.d.ts +0 -5
- package/types/hook.d.ts +1 -10
- package/types/plugin.d.ts +2 -96
- package/types/sync.d.ts +19 -25
- package/utils/convertBigIntFields.ts +38 -0
- package/utils/disableMenusGlob.ts +85 -0
- package/utils/importDefault.ts +21 -0
- package/utils/isDirentDirectory.ts +23 -0
- package/utils/loadMenuConfigs.ts +145 -0
- package/utils/processFields.ts +25 -0
- package/utils/scanAddons.ts +72 -0
- package/utils/scanFiles.ts +129 -21
- package/utils/scanSources.ts +64 -0
- package/utils/sortModules.ts +137 -0
- package/checks/checkApp.ts +0 -55
- package/docs/cipher.md +0 -582
- package/docs/database.md +0 -1176
- package/hooks/rateLimit.ts +0 -276
- package/sync/syncAll.ts +0 -35
- package/sync/syncDb/apply.ts +0 -192
- package/sync/syncDb/constants.ts +0 -119
- package/sync/syncDb/ddl.ts +0 -251
- package/sync/syncDb/helpers.ts +0 -84
- package/sync/syncDb/schema.ts +0 -202
- package/sync/syncDb/sqlite.ts +0 -48
- package/sync/syncDb/table.ts +0 -207
- package/sync/syncDb/tableCreate.ts +0 -163
- package/sync/syncDb/types.ts +0 -132
- package/sync/syncDb/version.ts +0 -69
- package/sync/syncDb.ts +0 -168
- package/tests/rateLimit-hook.test.ts +0 -477
- package/tests/syncDb-constants.test.ts +0 -130
- package/tests/syncDb-schema.test.ts +0 -179
- package/tests/syncDb-types.test.ts +0 -139
- package/utils/addonHelper.ts +0 -90
- package/utils/modules.ts +0 -98
- package/utils/route.ts +0 -23
package/checks/checkTable.ts
CHANGED
|
@@ -1,31 +1,7 @@
|
|
|
1
|
-
// 类型导入
|
|
2
1
|
import type { FieldDefinition } from "../types/validate.js";
|
|
3
|
-
|
|
4
|
-
// 内部依赖
|
|
5
|
-
import { existsSync } from "node:fs";
|
|
6
|
-
|
|
7
|
-
// 外部依赖
|
|
8
|
-
import { basename } from "pathe";
|
|
2
|
+
import type { ScanFileResult } from "../utils/scanFiles.js";
|
|
9
3
|
|
|
10
4
|
import { Logger } from "../lib/logger.js";
|
|
11
|
-
import { projectTableDir } from "../paths.js";
|
|
12
|
-
import { scanAddons, getAddonDir } from "../utils/addonHelper.js";
|
|
13
|
-
// 相对导入
|
|
14
|
-
import { scanFiles } from "../utils/scanFiles.js";
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* 表文件信息接口
|
|
18
|
-
*/
|
|
19
|
-
interface TableFileInfo {
|
|
20
|
-
/** 表文件路径 */
|
|
21
|
-
file: string;
|
|
22
|
-
/** 文件类型:project(项目)或 addon(组件) */
|
|
23
|
-
type: "project" | "addon";
|
|
24
|
-
/** 如果是 addon 类型,记录 addon 名称 */
|
|
25
|
-
addonName?: string;
|
|
26
|
-
/** 类型名称(用于日志) */
|
|
27
|
-
typeName: string;
|
|
28
|
-
}
|
|
29
5
|
|
|
30
6
|
/**
|
|
31
7
|
* 保留字段列表
|
|
@@ -65,202 +41,180 @@ const MAX_VARCHAR_LENGTH = 65535;
|
|
|
65
41
|
* 检查表定义文件
|
|
66
42
|
* @throws 当检查失败时抛出异常
|
|
67
43
|
*/
|
|
68
|
-
export async function checkTable(): Promise<void> {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const files = await scanFiles(projectTableDir, "*.json");
|
|
77
|
-
for (const { filePath } of files) {
|
|
78
|
-
allTableFiles.push({
|
|
79
|
-
file: filePath,
|
|
80
|
-
type: "project",
|
|
81
|
-
typeName: "项目"
|
|
82
|
-
});
|
|
83
|
-
}
|
|
44
|
+
export async function checkTable(tables: ScanFileResult[]): Promise<void> {
|
|
45
|
+
// 收集所有表文件
|
|
46
|
+
let hasError = false;
|
|
47
|
+
|
|
48
|
+
// 合并进行验证逻辑
|
|
49
|
+
for (const item of tables) {
|
|
50
|
+
if (item.type !== "table") {
|
|
51
|
+
continue;
|
|
84
52
|
}
|
|
85
53
|
|
|
86
|
-
|
|
87
|
-
const addons = scanAddons();
|
|
88
|
-
for (const addonName of addons) {
|
|
89
|
-
const addonTablesDir = getAddonDir(addonName, "tables");
|
|
54
|
+
const sourceName = typeof item.sourceName === "string" ? item.sourceName : "";
|
|
90
55
|
|
|
91
|
-
|
|
92
|
-
|
|
56
|
+
try {
|
|
57
|
+
const fileName = item.fileName;
|
|
58
|
+
const table = item.content || {};
|
|
59
|
+
// 1) 文件名小驼峰校验
|
|
60
|
+
if (!LOWER_CAMEL_CASE_REGEX.test(fileName)) {
|
|
61
|
+
Logger.warn(`${sourceName}表 ${fileName} 文件名必须使用小驼峰命名(例如 testCustomers.json)`);
|
|
62
|
+
hasError = true;
|
|
93
63
|
continue;
|
|
94
64
|
}
|
|
95
65
|
|
|
96
|
-
|
|
97
|
-
for (const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
}
|
|
66
|
+
// 检查 table 中的每个验证规则
|
|
67
|
+
for (const [colKey, fieldDef] of Object.entries(table)) {
|
|
68
|
+
if (typeof fieldDef !== "object" || fieldDef === null || Array.isArray(fieldDef)) {
|
|
69
|
+
Logger.warn(`${sourceName}表 ${fileName} 文件 ${colKey} 规则必须为对象`);
|
|
70
|
+
hasError = true;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
106
73
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
74
|
+
// 检查是否使用了保留字段
|
|
75
|
+
if (RESERVED_FIELDS.includes(colKey as any)) {
|
|
76
|
+
Logger.warn(`${sourceName}表 ${fileName} 文件包含保留字段 ${colKey},` + `不能在表定义中使用以下字段: ${RESERVED_FIELDS.join(", ")}`);
|
|
77
|
+
hasError = true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 直接使用字段对象
|
|
81
|
+
const field = fieldDef as FieldDefinition;
|
|
82
|
+
|
|
83
|
+
// 检查是否存在非法属性
|
|
84
|
+
const fieldKeys = Object.keys(field);
|
|
85
|
+
const illegalProps = fieldKeys.filter((key) => !ALLOWED_FIELD_PROPERTIES.includes(key as any));
|
|
86
|
+
if (illegalProps.length > 0) {
|
|
87
|
+
Logger.warn(`${sourceName}表 ${fileName} 文件 ${colKey} 包含非法属性: ${illegalProps.join(", ")},` + `允许的属性为: ${ALLOWED_FIELD_PROPERTIES.join(", ")}`);
|
|
88
|
+
hasError = true;
|
|
89
|
+
}
|
|
111
90
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
91
|
+
// 检查必填字段:name, type
|
|
92
|
+
if (!field.name || typeof field.name !== "string") {
|
|
93
|
+
Logger.warn(`${sourceName}表 ${fileName} 文件 ${colKey} 缺少必填字段 name 或类型错误`);
|
|
94
|
+
hasError = true;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (!field.type || typeof field.type !== "string") {
|
|
98
|
+
Logger.warn(`${sourceName}表 ${fileName} 文件 ${colKey} 缺少必填字段 type 或类型错误`);
|
|
116
99
|
hasError = true;
|
|
117
100
|
continue;
|
|
118
101
|
}
|
|
119
102
|
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
103
|
+
// 检查可选字段的类型
|
|
104
|
+
if (field.min !== undefined && !(field.min === null || typeof field.min === "number")) {
|
|
105
|
+
Logger.warn(`${sourceName}表 ${fileName} 文件 ${colKey} 字段 min 类型错误,必须为 null 或数字`);
|
|
106
|
+
hasError = true;
|
|
107
|
+
}
|
|
108
|
+
if (field.max !== undefined && !(field.max === null || typeof field.max === "number")) {
|
|
109
|
+
Logger.warn(`${sourceName}表 ${fileName} 文件 ${colKey} 字段 max 类型错误,必须为 null 或数字`);
|
|
110
|
+
hasError = true;
|
|
111
|
+
}
|
|
112
|
+
if (field.detail !== undefined && typeof field.detail !== "string") {
|
|
113
|
+
Logger.warn(`${sourceName}表 ${fileName} 文件 ${colKey} 字段 detail 类型错误,必须为字符串`);
|
|
114
|
+
hasError = true;
|
|
115
|
+
}
|
|
116
|
+
if (field.index !== undefined && typeof field.index !== "boolean") {
|
|
117
|
+
Logger.warn(`${sourceName}表 ${fileName} 文件 ${colKey} 字段 index 类型错误,必须为布尔值`);
|
|
118
|
+
hasError = true;
|
|
119
|
+
}
|
|
120
|
+
if (field.unique !== undefined && typeof field.unique !== "boolean") {
|
|
121
|
+
Logger.warn(`${sourceName}表 ${fileName} 文件 ${colKey} 字段 unique 类型错误,必须为布尔值`);
|
|
122
|
+
hasError = true;
|
|
123
|
+
}
|
|
124
|
+
if (field.nullable !== undefined && typeof field.nullable !== "boolean") {
|
|
125
|
+
Logger.warn(`${sourceName}表 ${fileName} 文件 ${colKey} 字段 nullable 类型错误,必须为布尔值`);
|
|
126
|
+
hasError = true;
|
|
127
|
+
}
|
|
128
|
+
if (field.unsigned !== undefined && typeof field.unsigned !== "boolean") {
|
|
129
|
+
Logger.warn(`${sourceName}表 ${fileName} 文件 ${colKey} 字段 unsigned 类型错误,必须为布尔值`);
|
|
130
|
+
hasError = true;
|
|
131
|
+
}
|
|
132
|
+
if (field.regexp !== undefined && field.regexp !== null && typeof field.regexp !== "string") {
|
|
133
|
+
Logger.warn(`${sourceName}表 ${fileName} 文件 ${colKey} 字段 regexp 类型错误,必须为 null 或字符串`);
|
|
134
|
+
hasError = true;
|
|
135
|
+
}
|
|
123
136
|
|
|
124
|
-
|
|
125
|
-
for (const [colKey, fieldDef] of Object.entries(table)) {
|
|
126
|
-
if (typeof fieldDef !== "object" || fieldDef === null || Array.isArray(fieldDef)) {
|
|
127
|
-
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 规则必须为对象`);
|
|
128
|
-
hasError = true;
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
137
|
+
const { name: fieldName, type: fieldType, min: fieldMin, max: fieldMax, default: fieldDefault } = field;
|
|
131
138
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
139
|
+
// 字段名称必须为中文、数字、字母、下划线、短横线、空格
|
|
140
|
+
if (!FIELD_NAME_REGEX.test(fieldName)) {
|
|
141
|
+
Logger.warn(`${sourceName}表 ${fileName} 文件 ${colKey} 字段名称 "${fieldName}" 格式错误,` + `必须为中文、数字、字母、下划线、短横线、空格`);
|
|
142
|
+
hasError = true;
|
|
143
|
+
}
|
|
137
144
|
|
|
138
|
-
|
|
139
|
-
|
|
145
|
+
// 字段类型必须为string,number,text,array_string,array_text之一
|
|
146
|
+
if (!FIELD_TYPES.includes(fieldType as any)) {
|
|
147
|
+
Logger.warn(`${sourceName}表 ${fileName} 文件 ${colKey} 字段类型 "${fieldType}" 格式错误,` + `必须为${FIELD_TYPES.join("、")}之一`);
|
|
148
|
+
hasError = true;
|
|
149
|
+
}
|
|
140
150
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
hasError = true;
|
|
147
|
-
}
|
|
151
|
+
// unsigned 仅对 number 类型有效(且仅 MySQL 语义上生效)
|
|
152
|
+
if (fieldType !== "number" && field.unsigned !== undefined) {
|
|
153
|
+
Logger.warn(`${sourceName}表 ${fileName} 文件 ${colKey} 字段类型为 ${fieldType},不允许设置 unsigned(仅 number 类型有效)`);
|
|
154
|
+
hasError = true;
|
|
155
|
+
}
|
|
148
156
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
if (!field.type || typeof field.type !== "string") {
|
|
156
|
-
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 缺少必填字段 type 或类型错误`);
|
|
157
|
-
hasError = true;
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
157
|
+
// 约束:unique 与 index 不能同时为 true(否则会重复索引),必须阻断启动。
|
|
158
|
+
if (field.unique === true && field.index === true) {
|
|
159
|
+
Logger.warn(`${sourceName}表 ${fileName} 文件 ${colKey} 同时设置了 unique=true 和 index=true,` + `unique 和 index 不能同时设置,请删除其一(否则会创建重复索引)`);
|
|
160
|
+
hasError = true;
|
|
161
|
+
}
|
|
160
162
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
163
|
+
// 约束:当最小值与最大值均为数字时,要求最小值 <= 最大值
|
|
164
|
+
if (fieldMin !== undefined && fieldMax !== undefined && fieldMin !== null && fieldMax !== null) {
|
|
165
|
+
if (fieldMin > fieldMax) {
|
|
166
|
+
Logger.warn(`${sourceName}表 ${fileName} 文件 ${colKey} 最小值 "${fieldMin}" 不能大于最大值 "${fieldMax}"`);
|
|
164
167
|
hasError = true;
|
|
165
168
|
}
|
|
166
|
-
|
|
167
|
-
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 类型联动校验 + 默认值规则
|
|
172
|
+
if (fieldType === "text" || fieldType === "array_text" || fieldType === "array_number_text") {
|
|
173
|
+
// text / array_text / array_number_text:min/max 必须为 null,默认值必须为 null,且不支持索引/唯一约束
|
|
174
|
+
if (fieldMin !== undefined && fieldMin !== null) {
|
|
175
|
+
Logger.warn(`${sourceName}表 ${fileName} 文件 ${colKey} 的 ${fieldType} 类型最小值应为 null,当前为 "${fieldMin}"`);
|
|
168
176
|
hasError = true;
|
|
169
177
|
}
|
|
170
|
-
if (
|
|
171
|
-
Logger.warn(`${
|
|
178
|
+
if (fieldMax !== undefined && fieldMax !== null) {
|
|
179
|
+
Logger.warn(`${sourceName}表 ${fileName} 文件 ${colKey} 的 ${fieldType} 类型最大长度应为 null,当前为 "${fieldMax}"`);
|
|
172
180
|
hasError = true;
|
|
173
181
|
}
|
|
174
|
-
if (
|
|
175
|
-
Logger.warn(`${
|
|
182
|
+
if (fieldDefault !== undefined && fieldDefault !== null) {
|
|
183
|
+
Logger.warn(`${sourceName}表 ${fileName} 文件 ${colKey} 为 ${fieldType} 类型,默认值必须为 null,当前为 "${fieldDefault}"`);
|
|
176
184
|
hasError = true;
|
|
177
185
|
}
|
|
178
|
-
|
|
179
|
-
|
|
186
|
+
|
|
187
|
+
if (field.index === true) {
|
|
188
|
+
Logger.warn(`${sourceName}表 ${fileName} 文件 ${colKey} 为 ${fieldType} 类型,不支持创建索引(index=true 无效)`);
|
|
180
189
|
hasError = true;
|
|
181
190
|
}
|
|
182
|
-
if (field.
|
|
183
|
-
Logger.warn(`${
|
|
191
|
+
if (field.unique === true) {
|
|
192
|
+
Logger.warn(`${sourceName}表 ${fileName} 文件 ${colKey} 为 ${fieldType} 类型,不支持唯一约束(unique=true 无效)`);
|
|
184
193
|
hasError = true;
|
|
185
194
|
}
|
|
186
|
-
|
|
187
|
-
|
|
195
|
+
} else if (fieldType === "string" || fieldType === "array_string" || fieldType === "array_number_string") {
|
|
196
|
+
if (fieldMax !== undefined && (fieldMax === null || typeof fieldMax !== "number")) {
|
|
197
|
+
Logger.warn(`${sourceName}表 ${fileName} 文件 ${colKey} 为 ${fieldType} 类型,` + `最大长度必须为数字,当前为 "${fieldMax}"`);
|
|
188
198
|
hasError = true;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 regexp 类型错误,必须为 null 或字符串`);
|
|
199
|
+
} else if (fieldMax !== undefined && fieldMax > MAX_VARCHAR_LENGTH) {
|
|
200
|
+
Logger.warn(`${sourceName}表 ${fileName} 文件 ${colKey} 最大长度 ${fieldMax} 越界,` + `${fieldType} 类型长度必须在 1..${MAX_VARCHAR_LENGTH} 范围内`);
|
|
192
201
|
hasError = true;
|
|
193
202
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if (!FIELD_NAME_REGEX.test(fieldName)) {
|
|
199
|
-
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段名称 "${fieldName}" 格式错误,` + `必须为中文、数字、字母、下划线、短横线、空格`);
|
|
200
|
-
hasError = true;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// 字段类型必须为string,number,text,array_string,array_text之一
|
|
204
|
-
if (!FIELD_TYPES.includes(fieldType as any)) {
|
|
205
|
-
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段类型 "${fieldType}" 格式错误,` + `必须为${FIELD_TYPES.join("、")}之一`);
|
|
203
|
+
} else if (fieldType === "number") {
|
|
204
|
+
// number 类型:default 如果存在,必须为 null 或 number
|
|
205
|
+
if (fieldDefault !== undefined && fieldDefault !== null && typeof fieldDefault !== "number") {
|
|
206
|
+
Logger.warn(`${sourceName}表 ${fileName} 文件 ${colKey} 为 number 类型,` + `默认值必须为数字或 null,当前为 "${fieldDefault}"`);
|
|
206
207
|
hasError = true;
|
|
207
208
|
}
|
|
208
|
-
|
|
209
|
-
// 检查 unique 和 index 冲突(警告但不阻断)
|
|
210
|
-
if (field.unique && field.index) {
|
|
211
|
-
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 同时设置了 unique=true 和 index=true,` + `unique 约束会自动创建唯一索引,index=true 将被忽略以避免重复索引`);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// 约束:当最小值与最大值均为数字时,要求最小值 <= 最大值
|
|
215
|
-
if (fieldMin !== undefined && fieldMax !== undefined && fieldMin !== null && fieldMax !== null) {
|
|
216
|
-
if (fieldMin > fieldMax) {
|
|
217
|
-
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 最小值 "${fieldMin}" 不能大于最大值 "${fieldMax}"`);
|
|
218
|
-
hasError = true;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// 类型联动校验 + 默认值规则
|
|
223
|
-
if (fieldType === "text") {
|
|
224
|
-
// text:min/max 应该为 null,默认值必须为 null
|
|
225
|
-
if (fieldMin !== undefined && fieldMin !== null) {
|
|
226
|
-
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 的 text 类型最小值应为 null,当前为 "${fieldMin}"`);
|
|
227
|
-
hasError = true;
|
|
228
|
-
}
|
|
229
|
-
if (fieldMax !== undefined && fieldMax !== null) {
|
|
230
|
-
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 的 text 类型最大长度应为 null,当前为 "${fieldMax}"`);
|
|
231
|
-
hasError = true;
|
|
232
|
-
}
|
|
233
|
-
if (fieldDefault !== undefined && fieldDefault !== null) {
|
|
234
|
-
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 为 text 类型,默认值必须为 null,当前为 "${fieldDefault}"`);
|
|
235
|
-
hasError = true;
|
|
236
|
-
}
|
|
237
|
-
} else if (fieldType === "string" || fieldType === "array_string" || fieldType === "array_number_string") {
|
|
238
|
-
if (fieldMax !== undefined && (fieldMax === null || typeof fieldMax !== "number")) {
|
|
239
|
-
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 为 ${fieldType} 类型,` + `最大长度必须为数字,当前为 "${fieldMax}"`);
|
|
240
|
-
hasError = true;
|
|
241
|
-
} else if (fieldMax !== undefined && fieldMax > MAX_VARCHAR_LENGTH) {
|
|
242
|
-
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 最大长度 ${fieldMax} 越界,` + `${fieldType} 类型长度必须在 1..${MAX_VARCHAR_LENGTH} 范围内`);
|
|
243
|
-
hasError = true;
|
|
244
|
-
}
|
|
245
|
-
} else if (fieldType === "number") {
|
|
246
|
-
// number 类型:default 如果存在,必须为 null 或 number
|
|
247
|
-
if (fieldDefault !== undefined && fieldDefault !== null && typeof fieldDefault !== "number") {
|
|
248
|
-
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 为 number 类型,` + `默认值必须为数字或 null,当前为 "${fieldDefault}"`);
|
|
249
|
-
hasError = true;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
209
|
}
|
|
253
|
-
} catch (error: any) {
|
|
254
|
-
Logger.error(`${item.typeName}表 ${fileName} 解析失败`, error);
|
|
255
|
-
hasError = true;
|
|
256
210
|
}
|
|
211
|
+
} catch (error: any) {
|
|
212
|
+
Logger.error(`${sourceName}表 ${item.fileName} 解析失败`, error);
|
|
213
|
+
hasError = true;
|
|
257
214
|
}
|
|
215
|
+
}
|
|
258
216
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
}
|
|
262
|
-
} catch (error: any) {
|
|
263
|
-
Logger.error("数据表定义检查过程中出错", error);
|
|
264
|
-
throw error;
|
|
217
|
+
if (hasError) {
|
|
218
|
+
throw new Error("表结构检查失败");
|
|
265
219
|
}
|
|
266
220
|
}
|
package/docs/README.md
CHANGED
|
@@ -2,18 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
> Befly 是基于 Bun 的高性能 API 框架
|
|
4
4
|
|
|
5
|
+
## 文档原则
|
|
6
|
+
|
|
7
|
+
- 文档只描述**当前实现**:不提供迁移指引、不保留兼容说明、不出现“已迁移/跳转页/占位页”。
|
|
8
|
+
- 如文档与代码行为不一致:**以代码为准**,应直接修正文档。
|
|
9
|
+
- 规则以“可执行/可验证”为标准:给出明确约束(例如 pathname-only、严格校验规则),避免含糊表述。
|
|
10
|
+
|
|
5
11
|
## 快速开始
|
|
6
12
|
|
|
7
13
|
- [Quickstart 快速入门](./guide/quickstart.md) - 5 分钟搭建第一个 API 服务
|
|
8
14
|
|
|
9
15
|
## 核心概念
|
|
10
16
|
|
|
11
|
-
| 文档 | 说明
|
|
12
|
-
| ---------------------------------------- |
|
|
13
|
-
| [API 开发](./api/api.md) | API
|
|
14
|
-
| [Table 表结构](./reference/table.md) | JSON 表定义格式、字段类型
|
|
15
|
-
| [Database 数据库](./plugins/database.md) | CRUD 操作、事务、批量操作
|
|
16
|
-
| [Config 配置](./reference/config.md) | 配置文件、环境分离
|
|
17
|
+
| 文档 | 说明 |
|
|
18
|
+
| ---------------------------------------- | --------------------------------------------------------------------- |
|
|
19
|
+
| [API 开发](./api/api.md) | API 定义、字段验证、权限控制([强约束清单](./api/api.md#强约束清单)) |
|
|
20
|
+
| [Table 表结构](./reference/table.md) | JSON 表定义格式、字段类型 |
|
|
21
|
+
| [Database 数据库](./plugins/database.md) | CRUD 操作、事务、批量操作 |
|
|
22
|
+
| [Config 配置](./reference/config.md) | 配置文件、环境分离 |
|
|
17
23
|
|
|
18
24
|
## 扩展开发
|
|
19
25
|
|
|
@@ -41,9 +47,9 @@
|
|
|
41
47
|
|
|
42
48
|
## 命令工具
|
|
43
49
|
|
|
44
|
-
| 文档 | 说明
|
|
45
|
-
| -------------------------------- |
|
|
46
|
-
| [Sync 同步](./reference/sync.md) | 数据库、API
|
|
50
|
+
| 文档 | 说明 |
|
|
51
|
+
| -------------------------------- | --------------------------------------------------------------------- |
|
|
52
|
+
| [Sync 同步](./reference/sync.md) | 数据库、API、菜单同步([强约束清单](./reference/sync.md#强约束清单)) |
|
|
47
53
|
|
|
48
54
|
## 实战示例
|
|
49
55
|
|
|
@@ -57,7 +63,7 @@
|
|
|
57
63
|
|
|
58
64
|
### 开发篇
|
|
59
65
|
|
|
60
|
-
2. **[API](./api/api.md)** - API
|
|
66
|
+
2. **[API](./api/api.md)** - API 路由定义、字段验证、权限控制、响应格式([强约束清单](./api/api.md#强约束清单))
|
|
61
67
|
3. **[Table](./reference/table.md)** - 表定义格式、字段类型、系统字段、命名规范
|
|
62
68
|
4. **[Database](./plugins/database.md)** - 数据库连接、CRUD 操作、事务处理、批量操作
|
|
63
69
|
5. **[Config](./reference/config.md)** - 配置文件结构、环境分离、运行时配置
|
|
@@ -77,7 +83,7 @@
|
|
|
77
83
|
|
|
78
84
|
### 运维篇
|
|
79
85
|
|
|
80
|
-
13. **[Sync](./reference/sync.md)** -
|
|
86
|
+
13. **[Sync](./reference/sync.md)** - syncTable、syncApi、syncMenu、syncDev、syncCache([强约束清单](./reference/sync.md#强约束清单))
|
|
81
87
|
|
|
82
88
|
## 常用链接
|
|
83
89
|
|
package/docs/api/api.md
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
- [目录](#目录)
|
|
9
9
|
- [概述](#概述)
|
|
10
10
|
- [核心特性](#核心特性)
|
|
11
|
+
- [强约束清单](#强约束清单)
|
|
11
12
|
- [目录结构](#目录结构-1)
|
|
12
13
|
- [项目 API](#项目-api)
|
|
13
14
|
- [Addon API](#addon-api)
|
|
@@ -78,6 +79,15 @@ Befly 框架的 API 系统是一套基于约定优于配置的接口开发体系
|
|
|
78
79
|
|
|
79
80
|
---
|
|
80
81
|
|
|
82
|
+
## 强约束清单
|
|
83
|
+
|
|
84
|
+
- **权限/路由匹配只看 pathname**:系统内部用于路由匹配与权限判断的值均为 `url.pathname`(例如 `/api/user/login`),与 method 无关。
|
|
85
|
+
- **禁止写法**:禁止把权限或路由路径写成 `POST/api/...` 或 `POST /api/...`(这些只是一种“请求行展示写法”,不能进入任何存储/配置/权限集合)。
|
|
86
|
+
- **routePath 必须严格合法**:必须以 `/api/` 开头、不得包含空格、不得出现 `/api//`。
|
|
87
|
+
- **同一路径多方法共用权限**:同一个 pathname(例如 `/api/user/login`)即使同时注册 GET/POST,也共用同一套权限集合。
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
81
91
|
## 目录结构
|
|
82
92
|
|
|
83
93
|
### 项目 API
|
|
@@ -139,7 +149,7 @@ export default {
|
|
|
139
149
|
fields: {}, // 字段定义(验证规则)
|
|
140
150
|
required: [], // 必填字段列表
|
|
141
151
|
rawBody: false // 是否保留原始请求体
|
|
142
|
-
//
|
|
152
|
+
// 缓存/限流:当前实现为 Hook 能力(见 hook 文档/配置),不再挂载在接口定义上
|
|
143
153
|
} as ApiRoute;
|
|
144
154
|
```
|
|
145
155
|
|
|
@@ -208,7 +218,7 @@ interface RequestContext {
|
|
|
208
218
|
/** 请求头 */
|
|
209
219
|
headers: Headers;
|
|
210
220
|
|
|
211
|
-
/** API
|
|
221
|
+
/** API 路由路径(url.pathname,例如 /api/user/login;与 method 无关) */
|
|
212
222
|
route: string;
|
|
213
223
|
|
|
214
224
|
/** 请求唯一 ID */
|
|
@@ -1276,6 +1286,8 @@ if (!ctx.user?.id) {
|
|
|
1276
1286
|
| `addonAdmin/apis/auth/login.ts` | `POST /api/addon/addonAdmin/auth/login` |
|
|
1277
1287
|
| `addonAdmin/apis/admin/list.ts` | `POST /api/addon/addonAdmin/admin/list` |
|
|
1278
1288
|
|
|
1289
|
+
> 注意:上表中的 `POST /api/...` 是“请求行示意”。系统内部生成并存储的 `ctx.route` / 数据库 `routePath` / 角色权限 `role.apis` / Redis 权限缓存都只使用 `url.pathname`(例如 `/api/user/login`),与 method 无关;权限数据禁止写成 `POST /api/...` 或 `POST/api/...`。
|
|
1290
|
+
|
|
1279
1291
|
### 多方法注册
|
|
1280
1292
|
|
|
1281
1293
|
当 `method: 'GET,POST'` 时,会同时注册两个路由:
|
|
@@ -1283,6 +1295,8 @@ if (!ctx.user?.id) {
|
|
|
1283
1295
|
- `GET /api/user/search`
|
|
1284
1296
|
- `POST /api/user/search`
|
|
1285
1297
|
|
|
1298
|
+
> 权限校验只看 pathname:如果同一个 pathname 同时注册了多个 method,它们共享同一套权限(以 `/api/user/search` 作为权限值)。
|
|
1299
|
+
|
|
1286
1300
|
---
|
|
1287
1301
|
|
|
1288
1302
|
## BeflyContext 对象
|
package/docs/guide/quickstart.md
CHANGED
|
@@ -121,6 +121,8 @@ export default {
|
|
|
121
121
|
| `apis/user/register.ts` | `POST /api/user/register` |
|
|
122
122
|
| `apis/article/list.ts` | `POST /api/article/list` |
|
|
123
123
|
|
|
124
|
+
> 注意:上面的写法是“HTTP 请求方法 + url.pathname”。系统内部生成并存储的 `routePath`、角色权限 `role.apis` 以及 Redis 权限缓存都只使用 `url.pathname`(例如 `/api/user/login`),与 method 无关,且禁止写成 `POST/api/...` 或 `POST /api/...`。
|
|
125
|
+
|
|
124
126
|
---
|
|
125
127
|
|
|
126
128
|
## 配置数据库
|
|
@@ -135,7 +137,7 @@ export default {
|
|
|
135
137
|
"type": "mysql",
|
|
136
138
|
"host": "127.0.0.1",
|
|
137
139
|
"port": 3306,
|
|
138
|
-
"
|
|
140
|
+
"username": "root",
|
|
139
141
|
"password": "your_password",
|
|
140
142
|
"database": "my_api"
|
|
141
143
|
},
|
|
@@ -208,15 +210,36 @@ CREATE DATABASE my_api CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
|
208
210
|
|
|
209
211
|
## 同步数据库
|
|
210
212
|
|
|
211
|
-
###
|
|
213
|
+
### 自动同步
|
|
212
214
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
215
|
+
服务启动时会在**主进程**自动执行同步流程:
|
|
216
|
+
|
|
217
|
+
1. `syncTable()`:同步表结构
|
|
218
|
+
2. `syncApi()`:同步接口定义
|
|
219
|
+
3. `syncMenu()`:同步菜单定义
|
|
220
|
+
4. `syncDev()`:同步开发账户/开发角色
|
|
221
|
+
5. `syncCache()`:缓存同步收尾(cacheApis + cacheMenus + rebuildRoleApiPermissions)
|
|
216
222
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
223
|
+
如需手动触发,可在代码中调用(一般不建议在请求路径中调用):
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
import { syncTable } from "../sync/syncTable.js";
|
|
227
|
+
import { syncApi } from "../sync/syncApi.js";
|
|
228
|
+
import { syncMenu } from "../sync/syncMenu.js";
|
|
229
|
+
import { syncDev } from "../sync/syncDev.js";
|
|
230
|
+
import { syncCache } from "../sync/syncCache.js";
|
|
231
|
+
import { checkMenu } from "../checks/checkMenu.js";
|
|
232
|
+
import { scanSources } from "../utils/scanSources.js";
|
|
233
|
+
|
|
234
|
+
// ctx:BeflyContext(需已具备 ctx.db / ctx.redis / ctx.cache / ctx.config)
|
|
235
|
+
const sources = await scanSources();
|
|
236
|
+
const checkedMenus = await checkMenu(sources.addons, { disableMenus: ctx.config.disableMenus || [] });
|
|
237
|
+
|
|
238
|
+
await syncTable(ctx, sources.tables);
|
|
239
|
+
await syncApi(ctx, sources.apis as any);
|
|
240
|
+
await syncMenu(ctx, checkedMenus);
|
|
241
|
+
await syncDev(ctx, { devEmail: ctx.config.devEmail, devPassword: ctx.config.devPassword });
|
|
242
|
+
await syncCache(ctx);
|
|
220
243
|
```
|
|
221
244
|
|
|
222
245
|
### 验证同步结果
|
|
@@ -299,8 +322,6 @@ curl -X POST http://localhost:3000/api/user/login \
|
|
|
299
322
|
```bash
|
|
300
323
|
# 开发
|
|
301
324
|
bun run dev # 启动开发服务
|
|
302
|
-
bun befly sync # 同步数据库
|
|
303
|
-
bun befly sync:db # 只同步表结构
|
|
304
325
|
|
|
305
326
|
# 生产
|
|
306
327
|
bun run build # 构建
|
package/docs/hooks/hook.md
CHANGED
|
@@ -541,7 +541,7 @@ export default hook;
|
|
|
541
541
|
"defaultLimit": 1000,
|
|
542
542
|
"defaultWindow": 60,
|
|
543
543
|
"key": "ip",
|
|
544
|
-
"skipRoutes": ["/api/health", "
|
|
544
|
+
"skipRoutes": ["/api/health", "/api/metrics"],
|
|
545
545
|
"rules": [
|
|
546
546
|
{
|
|
547
547
|
"route": "/api/auth/*",
|
|
@@ -550,7 +550,7 @@ export default hook;
|
|
|
550
550
|
"key": "ip"
|
|
551
551
|
},
|
|
552
552
|
{
|
|
553
|
-
"route": "
|
|
553
|
+
"route": "/api/order/create",
|
|
554
554
|
"limit": 5,
|
|
555
555
|
"window": 60,
|
|
556
556
|
"key": "user"
|