befly 3.9.39 → 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.
Files changed (141) hide show
  1. package/README.md +39 -8
  2. package/befly.config.ts +19 -2
  3. package/checks/checkApi.ts +79 -77
  4. package/checks/checkHook.ts +48 -0
  5. package/checks/checkMenu.ts +168 -0
  6. package/checks/checkPlugin.ts +48 -0
  7. package/checks/checkTable.ts +137 -183
  8. package/docs/README.md +1 -1
  9. package/docs/api/api.md +1 -1
  10. package/docs/guide/quickstart.md +16 -9
  11. package/docs/hooks/hook.md +2 -2
  12. package/docs/hooks/rateLimit.md +1 -1
  13. package/docs/infra/redis.md +7 -7
  14. package/docs/plugins/plugin.md +23 -21
  15. package/docs/quickstart.md +16 -9
  16. package/docs/reference/addon.md +12 -1
  17. package/docs/reference/config.md +13 -30
  18. package/docs/reference/sync.md +62 -193
  19. package/docs/reference/table.md +27 -29
  20. package/hooks/auth.ts +3 -4
  21. package/hooks/cors.ts +4 -6
  22. package/hooks/parser.ts +3 -4
  23. package/hooks/permission.ts +3 -4
  24. package/hooks/validator.ts +4 -5
  25. package/lib/cacheHelper.ts +89 -153
  26. package/lib/cacheKeys.ts +1 -1
  27. package/lib/connect.ts +9 -13
  28. package/lib/dbDialect.ts +285 -0
  29. package/lib/dbHelper.ts +179 -507
  30. package/lib/dbUtils.ts +450 -0
  31. package/lib/logger.ts +41 -5
  32. package/lib/redisHelper.ts +1 -0
  33. package/lib/sqlBuilder.ts +358 -58
  34. package/lib/sqlCheck.ts +136 -0
  35. package/lib/validator.ts +1 -1
  36. package/loader/loadApis.ts +23 -126
  37. package/loader/loadHooks.ts +31 -46
  38. package/loader/loadPlugins.ts +37 -52
  39. package/main.ts +58 -19
  40. package/package.json +24 -25
  41. package/paths.ts +14 -14
  42. package/plugins/cache.ts +12 -6
  43. package/plugins/cipher.ts +2 -2
  44. package/plugins/config.ts +6 -8
  45. package/plugins/db.ts +14 -19
  46. package/plugins/jwt.ts +6 -7
  47. package/plugins/logger.ts +7 -9
  48. package/plugins/redis.ts +8 -10
  49. package/plugins/tool.ts +3 -4
  50. package/router/api.ts +3 -2
  51. package/router/static.ts +7 -5
  52. package/sync/syncApi.ts +80 -235
  53. package/sync/syncCache.ts +16 -0
  54. package/sync/syncDev.ts +167 -202
  55. package/sync/syncMenu.ts +230 -444
  56. package/sync/syncTable.ts +1247 -0
  57. package/tests/_mocks/mockSqliteDb.ts +204 -0
  58. package/tests/addonHelper-cache.test.ts +32 -0
  59. package/tests/apiHandler-routePath-only.test.ts +32 -0
  60. package/tests/cacheHelper.test.ts +16 -51
  61. package/tests/checkApi-routePath-strict.test.ts +166 -0
  62. package/tests/checkMenu.test.ts +346 -0
  63. package/tests/checkTable-smoke.test.ts +157 -0
  64. package/tests/dbDialect-cache.test.ts +23 -0
  65. package/tests/dbDialect.test.ts +46 -0
  66. package/tests/dbHelper-advanced.test.ts +1 -1
  67. package/tests/dbHelper-all-array-types.test.ts +15 -15
  68. package/tests/dbHelper-batch-write.test.ts +90 -0
  69. package/tests/dbHelper-columns.test.ts +36 -54
  70. package/tests/dbHelper-execute.test.ts +26 -26
  71. package/tests/dbHelper-joins.test.ts +85 -176
  72. package/tests/fixtures/scanFilesAddon/node_modules/@befly-addon/demo/apis/sub/b.ts +3 -0
  73. package/tests/fixtures/scanFilesApis/a.ts +3 -0
  74. package/tests/fixtures/scanFilesApis/sub/b.ts +3 -0
  75. package/tests/loadPlugins-order-smoke.test.ts +75 -0
  76. package/tests/logger.test.ts +6 -6
  77. package/tests/redisHelper.test.ts +6 -1
  78. package/tests/scanFiles-routePath.test.ts +46 -0
  79. package/tests/smoke-sql.test.ts +24 -0
  80. package/tests/sqlBuilder-advanced.test.ts +18 -5
  81. package/tests/sqlBuilder.test.ts +24 -0
  82. package/tests/sync-init-guard.test.ts +105 -0
  83. package/tests/syncApi-insBatch-fields-consistent.test.ts +61 -0
  84. package/tests/syncApi-obsolete-records.test.ts +69 -0
  85. package/tests/syncApi-type-compat.test.ts +72 -0
  86. package/tests/syncDev-permissions.test.ts +81 -0
  87. package/tests/syncMenu-disableMenus-hard-delete.test.ts +88 -0
  88. package/tests/syncMenu-duplicate-path.test.ts +122 -0
  89. package/tests/syncMenu-obsolete-records.test.ts +161 -0
  90. package/tests/syncMenu-parentPath-from-tree.test.ts +75 -0
  91. package/tests/syncMenu-paths.test.ts +0 -9
  92. package/tests/{syncDb-apply.test.ts → syncTable-apply.test.ts} +14 -24
  93. package/tests/{syncDb-array-number.test.ts → syncTable-array-number.test.ts} +31 -31
  94. package/tests/syncTable-constants.test.ts +101 -0
  95. package/tests/syncTable-db-integration.test.ts +237 -0
  96. package/tests/{syncDb-ddl.test.ts → syncTable-ddl.test.ts} +67 -53
  97. package/tests/{syncDb-helpers.test.ts → syncTable-helpers.test.ts} +12 -26
  98. package/tests/syncTable-schema.test.ts +99 -0
  99. package/tests/syncTable-testkit.test.ts +25 -0
  100. package/tests/syncTable-types.test.ts +122 -0
  101. package/tests/tableRef-and-deserialize.test.ts +67 -0
  102. package/tsconfig.json +1 -1
  103. package/types/api.d.ts +1 -1
  104. package/types/befly.d.ts +13 -12
  105. package/types/cache.d.ts +2 -2
  106. package/types/context.d.ts +1 -1
  107. package/types/database.d.ts +0 -5
  108. package/types/hook.d.ts +1 -10
  109. package/types/plugin.d.ts +2 -96
  110. package/types/sync.d.ts +19 -25
  111. package/utils/convertBigIntFields.ts +38 -0
  112. package/utils/disableMenusGlob.ts +85 -0
  113. package/utils/importDefault.ts +21 -0
  114. package/utils/isDirentDirectory.ts +23 -0
  115. package/utils/loadMenuConfigs.ts +145 -0
  116. package/utils/processFields.ts +25 -0
  117. package/utils/scanAddons.ts +72 -0
  118. package/utils/scanFiles.ts +129 -21
  119. package/utils/scanSources.ts +64 -0
  120. package/utils/sortModules.ts +137 -0
  121. package/checks/checkApp.ts +0 -55
  122. package/hooks/rateLimit.ts +0 -276
  123. package/sync/syncAll.ts +0 -35
  124. package/sync/syncDb/apply.ts +0 -192
  125. package/sync/syncDb/constants.ts +0 -119
  126. package/sync/syncDb/ddl.ts +0 -251
  127. package/sync/syncDb/helpers.ts +0 -84
  128. package/sync/syncDb/schema.ts +0 -202
  129. package/sync/syncDb/sqlite.ts +0 -48
  130. package/sync/syncDb/table.ts +0 -207
  131. package/sync/syncDb/tableCreate.ts +0 -163
  132. package/sync/syncDb/types.ts +0 -132
  133. package/sync/syncDb/version.ts +0 -69
  134. package/sync/syncDb.ts +0 -168
  135. package/tests/rateLimit-hook.test.ts +0 -477
  136. package/tests/syncDb-constants.test.ts +0 -130
  137. package/tests/syncDb-schema.test.ts +0 -179
  138. package/tests/syncDb-types.test.ts +0 -139
  139. package/utils/addonHelper.ts +0 -90
  140. package/utils/modules.ts +0 -98
  141. package/utils/route.ts +0 -23
@@ -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
- try {
70
- // 收集所有表文件
71
- const allTableFiles: TableFileInfo[] = [];
72
- let hasError = false;
73
-
74
- // 收集项目表字段定义文件(如果目录存在)
75
- if (existsSync(projectTableDir)) {
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
- // 收集 addon 表字段定义文件
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
- // 检查 addon tables 目录是否存在
92
- if (!existsSync(addonTablesDir)) {
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
- const files = await scanFiles(addonTablesDir, "*.json");
97
- for (const { filePath } of files) {
98
- allTableFiles.push({
99
- file: filePath,
100
- type: "addon",
101
- typeName: `组件${addonName}`,
102
- addonName: addonName
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
- for (const item of allTableFiles) {
109
- const fileName = basename(item.file);
110
- const fileBaseName = basename(item.file, ".json");
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
- try {
113
- // 1) 文件名小驼峰校验
114
- if (!LOWER_CAMEL_CASE_REGEX.test(fileBaseName)) {
115
- Logger.warn(`${item.typeName}表 ${fileName} 文件名必须使用小驼峰命名(例如 testCustomers.json)`);
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
- // 动态导入 JSON 文件
121
- const tableModule = await import(item.file, { with: { type: "json" } });
122
- const table = tableModule.default;
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
- // 检查 table 中的每个验证规则
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
- if (RESERVED_FIELDS.includes(colKey as any)) {
134
- Logger.warn(`${item.typeName}表 ${fileName} 文件包含保留字段 ${colKey},` + `不能在表定义中使用以下字段: ${RESERVED_FIELDS.join(", ")}`);
135
- hasError = true;
136
- }
139
+ // 字段名称必须为中文、数字、字母、下划线、短横线、空格
140
+ if (!FIELD_NAME_REGEX.test(fieldName)) {
141
+ Logger.warn(`${sourceName}表 ${fileName} 文件 ${colKey} 字段名称 "${fieldName}" 格式错误,` + `必须为中文、数字、字母、下划线、短横线、空格`);
142
+ hasError = true;
143
+ }
137
144
 
138
- // 直接使用字段对象
139
- const field = fieldDef as FieldDefinition;
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
- const fieldKeys = Object.keys(field);
143
- const illegalProps = fieldKeys.filter((key) => !ALLOWED_FIELD_PROPERTIES.includes(key as any));
144
- if (illegalProps.length > 0) {
145
- Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 包含非法属性: ${illegalProps.join(", ")},` + `允许的属性为: ${ALLOWED_FIELD_PROPERTIES.join(", ")}`);
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
- // 检查必填字段:name, type
150
- if (!field.name || typeof field.name !== "string") {
151
- Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 缺少必填字段 name 或类型错误`);
152
- hasError = true;
153
- continue;
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
- if (field.min !== undefined && !(field.min === null || typeof field.min === "number")) {
163
- Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 min 类型错误,必须为 null 或数字`);
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
- if (field.max !== undefined && !(field.max === null || typeof field.max === "number")) {
167
- Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 max 类型错误,必须为 null 或数字`);
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 (field.detail !== undefined && typeof field.detail !== "string") {
171
- Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 detail 类型错误,必须为字符串`);
178
+ if (fieldMax !== undefined && fieldMax !== null) {
179
+ Logger.warn(`${sourceName}表 ${fileName} 文件 ${colKey} ${fieldType} 类型最大长度应为 null,当前为 "${fieldMax}"`);
172
180
  hasError = true;
173
181
  }
174
- if (field.index !== undefined && typeof field.index !== "boolean") {
175
- Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 index 类型错误,必须为布尔值`);
182
+ if (fieldDefault !== undefined && fieldDefault !== null) {
183
+ Logger.warn(`${sourceName}表 ${fileName} 文件 ${colKey} ${fieldType} 类型,默认值必须为 null,当前为 "${fieldDefault}"`);
176
184
  hasError = true;
177
185
  }
178
- if (field.unique !== undefined && typeof field.unique !== "boolean") {
179
- Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 unique 类型错误,必须为布尔值`);
186
+
187
+ if (field.index === true) {
188
+ Logger.warn(`${sourceName}表 ${fileName} 文件 ${colKey} 为 ${fieldType} 类型,不支持创建索引(index=true 无效)`);
180
189
  hasError = true;
181
190
  }
182
- if (field.nullable !== undefined && typeof field.nullable !== "boolean") {
183
- Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 nullable 类型错误,必须为布尔值`);
191
+ if (field.unique === true) {
192
+ Logger.warn(`${sourceName}表 ${fileName} 文件 ${colKey} ${fieldType} 类型,不支持唯一约束(unique=true 无效)`);
184
193
  hasError = true;
185
194
  }
186
- if (field.unsigned !== undefined && typeof field.unsigned !== "boolean") {
187
- Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 unsigned 类型错误,必须为布尔值`);
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
- if (field.regexp !== undefined && field.regexp !== null && typeof field.regexp !== "string") {
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
- const { name: fieldName, type: fieldType, min: fieldMin, max: fieldMax, default: fieldDefault } = field;
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
- if (hasError) {
260
- throw new Error("表结构检查失败");
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
@@ -77,7 +77,7 @@
77
77
 
78
78
  ### 运维篇
79
79
 
80
- 13. **[Sync](./reference/sync.md)** - syncDb、syncApi、syncMenu、syncDev 命令
80
+ 13. **[Sync](./reference/sync.md)** - syncTablesyncData(内部:syncApi、syncMenu、syncDev
81
81
 
82
82
  ## 常用链接
83
83
 
package/docs/api/api.md CHANGED
@@ -208,7 +208,7 @@ interface RequestContext {
208
208
  /** 请求头 */
209
209
  headers: Headers;
210
210
 
211
- /** API 路由路径(如 POST/api/user/login */
211
+ /** API 路由路径(url.pathname,例如 /api/user/login;与 method 无关) */
212
212
  route: string;
213
213
 
214
214
  /** 请求唯一 ID */
@@ -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
- ```bash
214
- # 全量同步(表结构 + API + 菜单 + 开发账户)
215
- bun befly sync
213
+ 服务启动时会在**主进程**自动执行同步流程:
214
+
215
+ 1. `syncTable()`:同步表结构
216
+ 2. `syncData()`:固定顺序执行 `syncApi` → `syncMenu` → `syncDev`
216
217
 
217
- # 或单独同步
218
- bun befly sync:db # 只同步表结构
219
- bun befly sync:api # 只同步 API 路由
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 # 构建
@@ -541,7 +541,7 @@ export default hook;
541
541
  "defaultLimit": 1000,
542
542
  "defaultWindow": 60,
543
543
  "key": "ip",
544
- "skipRoutes": ["/api/health", "GET/api/metrics"],
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": "POST/api/order/create",
553
+ "route": "/api/order/create",
554
554
  "limit": 5,
555
555
  "window": 60,
556
556
  "key": "user"
@@ -22,7 +22,7 @@
22
22
  "key": "ip"
23
23
  }
24
24
  ],
25
- "skipRoutes": ["GET/api/health"]
25
+ "skipRoutes": ["/api/health"]
26
26
  }
27
27
  }
28
28
  ```
@@ -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: ["GET/api/user", "POST/api/user"] },
225
- { key: "role:editor:apis", members: ["GET/api/article", "POST/api/article"] }
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: "GET/api/user" },
235
- { key: "role:admin:apis", member: "DELETE/api/user" }
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, "POST/api/user/add");
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
- // 返回: ['GET/api/user/list', 'POST/api/user/add', ...]
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", "POST/api/user/add");
483
+ const hasPermission = await befly.cache.checkRolePermission("admin", "/api/user/add");
484
484
  // 返回: true 或 false
485
485
  ```
486
486