befly 3.9.40 → 3.10.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 +39 -8
- 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 +1 -1
- package/docs/api/api.md +1 -1
- package/docs/guide/quickstart.md +16 -9
- package/docs/hooks/hook.md +2 -2
- package/docs/hooks/rateLimit.md +1 -1
- package/docs/infra/redis.md +7 -7
- package/docs/plugins/plugin.md +23 -21
- package/docs/quickstart.md +16 -9
- package/docs/reference/addon.md +12 -1
- package/docs/reference/config.md +13 -30
- package/docs/reference/sync.md +62 -193
- package/docs/reference/table.md +27 -29
- 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/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
package/docs/api/api.md
CHANGED
package/docs/guide/quickstart.md
CHANGED
|
@@ -208,15 +208,24 @@ CREATE DATABASE my_api CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
|
208
208
|
|
|
209
209
|
## 同步数据库
|
|
210
210
|
|
|
211
|
-
###
|
|
211
|
+
### 自动同步
|
|
212
212
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
213
|
+
服务启动时会在**主进程**自动执行同步流程:
|
|
214
|
+
|
|
215
|
+
1. `syncTable()`:同步表结构
|
|
216
|
+
2. `syncData()`:固定顺序执行 `syncApi` → `syncMenu` → `syncDev`
|
|
216
217
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
218
|
+
如需手动触发,可在代码中调用(一般不建议在请求路径中调用):
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
import { syncData } from "../sync/syncData.js";
|
|
222
|
+
import { syncTable } from "../sync/syncTable.js";
|
|
223
|
+
import { scanSources } from "../utils/scanSources.js";
|
|
224
|
+
|
|
225
|
+
// ctx:BeflyContext(需已具备 ctx.db / ctx.redis / ctx.config)
|
|
226
|
+
const sources = await scanSources();
|
|
227
|
+
await syncTable(ctx, sources.tables);
|
|
228
|
+
await syncData();
|
|
220
229
|
```
|
|
221
230
|
|
|
222
231
|
### 验证同步结果
|
|
@@ -299,8 +308,6 @@ curl -X POST http://localhost:3000/api/user/login \
|
|
|
299
308
|
```bash
|
|
300
309
|
# 开发
|
|
301
310
|
bun run dev # 启动开发服务
|
|
302
|
-
bun befly sync # 同步数据库
|
|
303
|
-
bun befly sync:db # 只同步表结构
|
|
304
311
|
|
|
305
312
|
# 生产
|
|
306
313
|
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"
|
package/docs/hooks/rateLimit.md
CHANGED
package/docs/infra/redis.md
CHANGED
|
@@ -221,8 +221,8 @@ const count = await befly.redis.expireBatch([
|
|
|
221
221
|
|
|
222
222
|
```typescript
|
|
223
223
|
const count = await befly.redis.saddBatch([
|
|
224
|
-
{ key: "role:admin:apis", members: ["
|
|
225
|
-
{ key: "role:editor:apis", members: ["
|
|
224
|
+
{ key: "role:admin:apis", members: ["/api/user"] },
|
|
225
|
+
{ key: "role:editor:apis", members: ["/api/article"] }
|
|
226
226
|
]);
|
|
227
227
|
// 返回: 成功添加的总成员数量
|
|
228
228
|
```
|
|
@@ -231,8 +231,8 @@ const count = await befly.redis.saddBatch([
|
|
|
231
231
|
|
|
232
232
|
```typescript
|
|
233
233
|
const results = await befly.redis.sismemberBatch([
|
|
234
|
-
{ key: "role:admin:apis", member: "
|
|
235
|
-
{ key: "role:admin:apis", member: "
|
|
234
|
+
{ key: "role:admin:apis", member: "/api/user" },
|
|
235
|
+
{ key: "role:admin:apis", member: "/api/user/delete" }
|
|
236
236
|
]);
|
|
237
237
|
// 返回: [true, false]
|
|
238
238
|
```
|
|
@@ -333,7 +333,7 @@ return befly.tool.No("请求过于频繁");
|
|
|
333
333
|
```typescript
|
|
334
334
|
// 极简方案:每个角色一个 Set
|
|
335
335
|
const roleApisKey = CacheKeys.roleApis("admin");
|
|
336
|
-
const hasPermission = await befly.redis.sismember(roleApisKey, "
|
|
336
|
+
const hasPermission = await befly.redis.sismember(roleApisKey, "/api/user/add");
|
|
337
337
|
// 返回: true
|
|
338
338
|
```
|
|
339
339
|
|
|
@@ -473,14 +473,14 @@ const menus = await befly.cache.getMenus();
|
|
|
473
473
|
|
|
474
474
|
// 获取角色权限
|
|
475
475
|
const permissions = await befly.cache.getRolePermissions("admin");
|
|
476
|
-
// 返回: ['
|
|
476
|
+
// 返回: ['/api/user/list', '/api/user/add', ...]
|
|
477
477
|
```
|
|
478
478
|
|
|
479
479
|
### 权限检查
|
|
480
480
|
|
|
481
481
|
```typescript
|
|
482
482
|
// 检查角色是否有指定接口权限
|
|
483
|
-
const hasPermission = await befly.cache.checkRolePermission("admin", "
|
|
483
|
+
const hasPermission = await befly.cache.checkRolePermission("admin", "/api/user/add");
|
|
484
484
|
// 返回: true 或 false
|
|
485
485
|
```
|
|
486
486
|
|