befly 3.76.0 → 3.76.2

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 (155) hide show
  1. package/Befly.js +159 -0
  2. package/README.md +50 -11
  3. package/apis/admin/delete.js +1 -3
  4. package/apis/admin/insert.js +2 -1
  5. package/apis/admin/select.js +2 -7
  6. package/apis/admin/update.js +3 -8
  7. package/apis/api/select.js +2 -7
  8. package/apis/auth/login.js +16 -20
  9. package/apis/dict/delete.js +1 -1
  10. package/apis/dict/detail.js +1 -3
  11. package/apis/dict/select.js +1 -2
  12. package/apis/dict/update.js +1 -1
  13. package/apis/dictType/delete.js +1 -1
  14. package/apis/dictType/detail.js +1 -1
  15. package/apis/dictType/insert.js +1 -1
  16. package/apis/dictType/select.js +2 -7
  17. package/apis/dictType/update.js +2 -2
  18. package/apis/email/config.js +4 -7
  19. package/apis/email/logList.js +2 -7
  20. package/apis/email/send.js +1 -2
  21. package/apis/email/verify.js +1 -3
  22. package/apis/loginLog/select.js +2 -7
  23. package/apis/menu/select.js +2 -7
  24. package/apis/operateLog/select.js +2 -7
  25. package/apis/role/apiSave.js +2 -2
  26. package/apis/role/menuSave.js +2 -2
  27. package/apis/role/select.js +2 -7
  28. package/apis/source/imageList.js +2 -5
  29. package/apis/tongJi/dailyReport.js +12 -13
  30. package/apis/tongJi/dailyReportSelect.js +1 -2
  31. package/apis/tongJi/dailyStats.js +1 -1
  32. package/apis/tongJi/dailyStatsDistribution.js +2 -2
  33. package/apis/tongJi/errorList.js +1 -2
  34. package/apis/tongJi/errorReport.js +1 -2
  35. package/apis/tongJi/projectDelete.js +1 -3
  36. package/apis/tongJi/projectSelect.js +2 -4
  37. package/apis/tongJi/projectUpdate.js +2 -2
  38. package/apis/upload/file.js +7 -5
  39. package/checks/api.js +43 -66
  40. package/checks/config.js +17 -12
  41. package/checks/corns.js +28 -46
  42. package/checks/field.js +18 -0
  43. package/checks/items.js +16 -60
  44. package/checks/menu.js +28 -39
  45. package/checks/table.js +106 -26
  46. package/checks/validation.js +13 -0
  47. package/configs/beflyConfig.json +6 -6
  48. package/configs/constConfig.js +2 -63
  49. package/exports.js +2 -0
  50. package/hooks/auth.js +11 -7
  51. package/hooks/parser.js +55 -59
  52. package/hooks/permission.js +5 -2
  53. package/hooks/rateLimit.js +25 -62
  54. package/hooks/validator.js +43 -31
  55. package/index.js +43 -344
  56. package/{lib → libs}/cacheHelper.js +3 -8
  57. package/libs/cron.js +60 -0
  58. package/libs/logger/index.js +1 -0
  59. package/libs/logger/logger.js +245 -0
  60. package/libs/logger/sanitize.js +115 -0
  61. package/{lib → libs/mysql}/dbHelper.js +100 -73
  62. package/{lib → libs/mysql}/dbParse.js +16 -9
  63. package/{lib → libs/mysql}/dbUtil.js +15 -32
  64. package/libs/mysql/error.js +29 -0
  65. package/libs/mysql/index.js +6 -0
  66. package/libs/mysql/is.js +29 -0
  67. package/libs/mysql/mysql.js +43 -0
  68. package/libs/mysql/sql/identifier.js +85 -0
  69. package/libs/mysql/sql/index.js +2 -0
  70. package/{lib → libs/mysql/sql}/sqlBuilder.js +2 -3
  71. package/libs/mysql/util.js +44 -0
  72. package/libs/redis/index.js +2 -0
  73. package/libs/redis/redis.js +204 -0
  74. package/libs/redis/redisError.js +8 -0
  75. package/libs/tableModel.js +72 -0
  76. package/libs/tool.js +33 -0
  77. package/libs/validator/compiler.js +248 -0
  78. package/libs/validator/index.js +10 -0
  79. package/libs/validator/parser.js +173 -0
  80. package/libs/validator/util.js +5 -0
  81. package/package.json +10 -8
  82. package/paths.js +2 -3
  83. package/plugins/cache.js +2 -1
  84. package/plugins/email.js +4 -4
  85. package/plugins/mysql.js +5 -15
  86. package/plugins/redis.js +4 -12
  87. package/router/api.js +14 -10
  88. package/router/root.js +14 -0
  89. package/router/static.js +3 -3
  90. package/schemas/api.json +99 -0
  91. package/schemas/config.json +255 -0
  92. package/schemas/corn.json +39 -0
  93. package/schemas/item.json +43 -0
  94. package/schemas/menu.json +44 -0
  95. package/schemas/table.json +80 -0
  96. package/scripts/syncDb/apply.js +87 -0
  97. package/scripts/syncDb/ddl.js +43 -0
  98. package/scripts/syncDb/index.js +175 -30
  99. package/scripts/syncDb/model.js +60 -0
  100. package/scripts/syncDb/plan.js +160 -0
  101. package/scripts/syncDb/preflight.js +98 -0
  102. package/scripts/syncDb/report.js +177 -162
  103. package/scripts/syncDb/snapshot.js +118 -0
  104. package/sync/syncUtil.js +7 -13
  105. package/tables/admin.json +51 -53
  106. package/tables/api.json +29 -40
  107. package/tables/dailyReport.json +85 -73
  108. package/tables/dict.json +27 -39
  109. package/tables/dictType.json +20 -34
  110. package/tables/emailLog.json +49 -47
  111. package/tables/errorReport.json +86 -60
  112. package/tables/file.json +47 -50
  113. package/tables/loginLog.json +68 -55
  114. package/tables/menu.json +20 -34
  115. package/tables/operateLog.json +53 -47
  116. package/tables/project.json +18 -30
  117. package/tables/role.json +33 -37
  118. package/utils/cors.js +37 -41
  119. package/utils/deepMerge.js +3 -1
  120. package/utils/error.js +1 -56
  121. package/utils/id.js +44 -0
  122. package/utils/is.js +6 -243
  123. package/utils/response.js +12 -1
  124. package/utils/scanFiles.js +13 -4
  125. package/utils/scanSources.js +58 -20
  126. package/utils/util.js +11 -163
  127. package/apis/_apis.js +0 -20
  128. package/apis/auth/sendSmsCode.js +0 -24
  129. package/apis/role/delete.js +0 -41
  130. package/apis/role/insert.js +0 -45
  131. package/apis/role/save.js +0 -36
  132. package/apis/role/update.js +0 -66
  133. package/configs/regexpAlias.json +0 -55
  134. package/lib/connect.js +0 -119
  135. package/lib/logger.js +0 -430
  136. package/lib/redisHelper.js +0 -399
  137. package/lib/validator.js +0 -274
  138. package/lib/xmlParse.js +0 -149
  139. package/plugins/config.js +0 -13
  140. package/plugins/cron.js +0 -115
  141. package/plugins/logger.js +0 -17
  142. package/plugins/tool.js +0 -131
  143. package/plugins/xmlParse.js +0 -10
  144. package/schemas/config.js +0 -67
  145. package/scripts/syncDb/context.js +0 -120
  146. package/scripts/syncDb/diff.js +0 -279
  147. package/scripts/syncDb/transform.js +0 -113
  148. package/sql/befly.sql +0 -271
  149. package/utils/email.js +0 -3
  150. package/utils/fieldClear.js +0 -22
  151. package/utils/formatValidationIssues.js +0 -28
  152. package/utils/getClientIp.js +0 -47
  153. package/utils/loggerUtils.js +0 -196
  154. package/utils/regexpUtil.js +0 -52
  155. /package/{lib → libs}/smtpText.js +0 -0
@@ -11,9 +11,9 @@ export default {
11
11
  roleCode: roleTable.code,
12
12
  menuPaths: roleTable.menus
13
13
  },
14
- required: ["roleCode"],
14
+ required: ["roleCode", "menuPaths"],
15
15
  handler: async (befly, ctx) => {
16
- const menuPaths = ctx.body.menuPaths || [];
16
+ const menuPaths = ctx.body.menuPaths;
17
17
 
18
18
  const role = await getRoleByCode(befly, ctx.body.roleCode);
19
19
 
@@ -1,15 +1,10 @@
1
- import { queryFields } from "#befly/apis/_apis.js";
2
- import roleTable from "#befly/tables/role.json";
3
-
4
1
  export default {
5
2
  name: "获取角色列表",
3
+ category: "select",
6
4
  method: "POST",
7
5
  body: "none",
8
6
  auth: true,
9
- fields: {
10
- ...queryFields,
11
- state: roleTable.state
12
- },
7
+ fields: {},
13
8
  required: [],
14
9
  handler: async (befly, ctx) => {
15
10
  const roles = await befly.mysql.getList({
@@ -1,13 +1,10 @@
1
- import { queryFields } from "#befly/apis/_apis.js";
2
-
3
1
  export default {
4
2
  name: "获取图库列表",
3
+ category: "select",
5
4
  method: "POST",
6
5
  body: "none",
7
6
  auth: true,
8
- fields: {
9
- ...queryFields
10
- },
7
+ fields: {},
11
8
  required: [],
12
9
  handler: async (befly, ctx) => {
13
10
  const result = await befly.mysql.getList({
@@ -1,4 +1,3 @@
1
- import { Logger } from "#befly/lib/logger.js";
2
1
  import { isValidPositiveInt } from "#befly/utils/is.js";
3
2
 
4
3
  import { getTongJiDateYmdNumber } from "./_tongJi.js";
@@ -11,17 +10,17 @@ export default {
11
10
  body: "none",
12
11
  auth: false,
13
12
  fields: {
14
- clientType: { name: "客户端类型", input: "enum", check: "browser|vscode|desktop|miniapp|app", min: 3, max: 20 },
15
- clientId: { name: "客户端标识", input: "string", min: 0, max: 120 },
16
- productName: { name: "产品名称", input: "string", min: 0, max: 100 },
17
- productCode: { name: "产品代号", input: "string", min: 0, max: 100 },
18
- productVersion: { name: "产品版本", input: "string", min: 0, max: 50 },
19
- clientVersion: { name: "客户端版本", input: "string", min: 0, max: 100 },
20
- os: { name: "操作系统", input: "string", min: 0, max: 100 },
21
- deviceId: { name: "设备标识", input: "string", min: 0, max: 120 },
22
- brand: { name: "设备品牌", input: "string", min: 0, max: 100 },
23
- model: { name: "设备型号", input: "string", min: 0, max: 100 },
24
- system: { name: "系统", input: "string", min: 0, max: 100 }
13
+ clientType: { name: "客户端类型", paramType: "string", enum: ["browser", "vscode", "desktop", "miniapp", "app"], minValue: 3, maxValue: 20 },
14
+ clientId: { name: "客户端标识", paramType: "string", minValue: 0, maxValue: 120 },
15
+ productName: { name: "产品名称", paramType: "string", minValue: 0, maxValue: 100 },
16
+ productCode: { name: "产品代号", paramType: "string", minValue: 0, maxValue: 100 },
17
+ productVersion: { name: "产品版本", paramType: "string", minValue: 0, maxValue: 50 },
18
+ clientVersion: { name: "客户端版本", paramType: "string", minValue: 0, maxValue: 100 },
19
+ os: { name: "操作系统", paramType: "string", minValue: 0, maxValue: 100 },
20
+ deviceId: { name: "设备标识", paramType: "string", minValue: 0, maxValue: 120 },
21
+ brand: { name: "设备品牌", paramType: "string", minValue: 0, maxValue: 100 },
22
+ model: { name: "设备型号", paramType: "string", minValue: 0, maxValue: 100 },
23
+ system: { name: "系统", paramType: "string", minValue: 0, maxValue: 100 }
25
24
  },
26
25
  required: ["clientType"],
27
26
  handler: async (befly, ctx) => {
@@ -84,7 +83,7 @@ export default {
84
83
  await befly.mysql.insData({ table: "beflyDailyReport", data: row });
85
84
  stored = true;
86
85
  } catch (error) {
87
- Logger.error("daily_report 落盘失败", error);
86
+ befly.logger.error("daily_report 落盘失败", error);
88
87
  }
89
88
  }
90
89
 
@@ -1,13 +1,12 @@
1
- import { queryFields } from "#befly/apis/_apis.js";
2
1
  import dailyReportTable from "#befly/tables/dailyReport.json";
3
2
 
4
3
  export default {
5
4
  name: "获取日报记录列表",
5
+ category: "select",
6
6
  method: "POST",
7
7
  body: "none",
8
8
  auth: true,
9
9
  fields: {
10
- ...queryFields,
11
10
  productCode: dailyReportTable.productCode,
12
11
  clientType: dailyReportTable.clientType,
13
12
  reportDate: dailyReportTable.reportDate,
@@ -137,7 +137,7 @@ export default {
137
137
  body: "none",
138
138
  auth: true,
139
139
  fields: {
140
- productCode: { name: "产品代号", input: "string", min: 0, max: 100 }
140
+ productCode: { name: "产品代号", paramType: "string", minValue: 0, maxValue: 100 }
141
141
  },
142
142
  required: [],
143
143
  handler: async (befly, ctx) => {
@@ -62,8 +62,8 @@ export default {
62
62
  body: "none",
63
63
  auth: true,
64
64
  fields: {
65
- productCode: { name: "产品代号", input: "string", min: 1, max: 100 },
66
- dateRange: { name: "日期范围", input: "enum", check: "today|yesterday|dayBeforeYesterday|recent30|month", min: 1, max: 20 }
65
+ productCode: { name: "产品代号", paramType: "string", minValue: 1, maxValue: 100 },
66
+ dateRange: { name: "日期范围", paramType: "string", enum: ["today", "yesterday", "dayBeforeYesterday", "recent30", "month"], minValue: 1, maxValue: 20 }
67
67
  },
68
68
  required: ["productCode", "dateRange"],
69
69
  handler: async (befly, ctx) => {
@@ -1,4 +1,3 @@
1
- import { queryFields } from "#befly/apis/_apis.js";
2
1
  import errorReportTable from "#befly/tables/errorReport.json";
3
2
 
4
3
  const NUMBER_FIELDS = ["reportTime", "firstReportTime", "bucketTime", "bucketDate"];
@@ -7,11 +6,11 @@ const KEYWORD_FIELDS = ["pagePath", "pageName", "message", "errorType", "product
7
6
 
8
7
  export default {
9
8
  name: "获取错误报告列表",
9
+ category: "select",
10
10
  method: "POST",
11
11
  body: "none",
12
12
  auth: true,
13
13
  fields: {
14
- ...queryFields,
15
14
  errorType: errorReportTable.errorType,
16
15
  productName: errorReportTable.productName,
17
16
  productCode: errorReportTable.productCode,
@@ -1,4 +1,3 @@
1
- import { Logger } from "#befly/lib/logger.js";
2
1
  import errorReportTable from "#befly/tables/errorReport.json";
3
2
  import { getDateYmdNumber, getTimeBucketStart } from "#befly/utils/datetime.js";
4
3
  import { isValidPositiveInt } from "#befly/utils/is.js";
@@ -111,7 +110,7 @@ export default {
111
110
  await befly.mysql.insData({ table: "beflyErrorReport", data: data });
112
111
  stored = true;
113
112
  } catch (error) {
114
- Logger.error("errorReport 插入失败", error);
113
+ befly.logger.error("errorReport 插入失败", error);
115
114
  }
116
115
  }
117
116
 
@@ -1,12 +1,10 @@
1
- import projectTable from "#befly/tables/project.json";
2
-
3
1
  export default {
4
2
  name: "删除产品",
5
3
  method: "POST",
6
4
  body: "none",
7
5
  auth: true,
8
6
  fields: {
9
- id: projectTable.id
7
+ id: { name: "ID", paramType: "integer", minValue: 1 }
10
8
  },
11
9
  required: ["id"],
12
10
  handler: async (befly, ctx) => {
@@ -1,15 +1,13 @@
1
- import { queryFields } from "#befly/apis/_apis.js";
2
1
  import projectTable from "#befly/tables/project.json";
3
2
 
4
3
  export default {
5
4
  name: "获取产品列表",
5
+ category: "select",
6
6
  method: "POST",
7
7
  body: "none",
8
8
  auth: true,
9
9
  fields: {
10
- ...queryFields,
11
- code: projectTable.code,
12
- state: projectTable.state
10
+ code: projectTable.code
13
11
  },
14
12
  required: [],
15
13
  handler: async (befly, ctx) => {
@@ -6,11 +6,11 @@ export default {
6
6
  body: "none",
7
7
  auth: true,
8
8
  fields: {
9
- id: projectTable.id,
9
+ id: { name: "ID", paramType: "integer", minValue: 1 },
10
10
  code: projectTable.code,
11
11
  name: projectTable.name,
12
12
  sort: projectTable.sort,
13
- state: projectTable.state
13
+ state: { name: "状态", paramType: "integer" }
14
14
  },
15
15
  required: ["id"],
16
16
  handler: async (befly, ctx) => {
@@ -1,7 +1,6 @@
1
- import { mkdirSync } from "node:fs";
1
+ import { existsSync, mkdirSync, unlinkSync } from "node:fs";
2
2
  import { extname, join } from "node:path";
3
3
 
4
- import { Logger } from "#befly/lib/logger.js";
5
4
  import { getAppPublicDir } from "#befly/paths.js";
6
5
  import { getMonthDir } from "#befly/utils/datetime.js";
7
6
 
@@ -13,6 +12,7 @@ export default {
13
12
  fields: {},
14
13
  required: [],
15
14
  handler: async (befly, ctx) => {
15
+ let savedFilePath;
16
16
  try {
17
17
  const maxFileSizeMb = Number(befly.config.upload.maxSize);
18
18
  const maxFileSize = maxFileSizeMb * 1024 * 1024;
@@ -78,7 +78,8 @@ export default {
78
78
  const isImage = fileType === "image" ? 1 : 0;
79
79
 
80
80
  mkdirSync(uploadDir, { recursive: true });
81
- await Bun.write(join(uploadDir, savedFileName), rawBuffer);
81
+ savedFilePath = join(uploadDir, savedFileName);
82
+ await Bun.write(savedFilePath, rawBuffer);
82
83
 
83
84
  const insertRes = await befly.mysql.insData({
84
85
  table: "beflyFile",
@@ -96,7 +97,7 @@ export default {
96
97
  });
97
98
 
98
99
  if (!insertRes.data) {
99
- return befly.tool.No("资源记录写入失败");
100
+ throw new Error("资源记录写入失败");
100
101
  }
101
102
 
102
103
  return befly.tool.Yes("上传成功", {
@@ -112,7 +113,8 @@ export default {
112
113
  url: absoluteFileUrl
113
114
  });
114
115
  } catch (error) {
115
- Logger.error("上传文件失败", error, {
116
+ if (savedFilePath && existsSync(savedFilePath)) unlinkSync(savedFilePath);
117
+ befly.logger.error("上传文件失败", error, {
116
118
  apiPath: ctx.apiPath,
117
119
  userId: ctx.userId,
118
120
  roleType: ctx.roleType
package/checks/api.js CHANGED
@@ -1,74 +1,51 @@
1
- import { Logger } from "../lib/logger.js";
2
- import { validateFieldRule } from "../lib/validator.js";
3
- import { formatValidationIssues } from "../utils/formatValidationIssues.js";
4
- import { isBoolean, isFunction, isLowerCamelCaseName, isPlainObject, isString } from "../utils/is.js";
5
-
6
- const apiPathRegex = /^\/api\/[a-z][a-zA-Z0-9]*(?:\/[a-z][a-zA-Z0-9]*)*$/;
7
- const apiKeys = new Set(["source", "type", "filePath", "relativePath", "fileName", "name", "handler", "apiPath", "method", "body", "auth", "fields", "required"]);
8
-
9
- function checkNoTrimString(issues, value, path, min = 0, max = Number.MAX_SAFE_INTEGER) {
10
- if (!isString(value)) {
11
- issues.push({ path: path, message: "必须是字符串" });
12
- return;
13
- }
14
- if (value !== "" && value.trim() !== value) issues.push({ path: path, message: "不允许首尾空格" });
15
- if (value.length < min) issues.push({ path: path, message: `长度不能少于${min}个字符` });
16
- if (value.length > max) issues.push({ path: path, message: `长度不能超过${max}个字符` });
17
- }
18
-
19
- function checkApiItem(api, index) {
20
- const issues = [];
21
- if (!isPlainObject(api)) {
22
- issues.push({ path: [index], message: "必须是对象格式" });
23
- return issues;
24
- }
25
-
26
- for (const key of Object.keys(api)) {
27
- if (!apiKeys.has(key)) issues.push({ path: [index, key], message: "不允许出现" });
28
- }
29
- for (const key of ["source", "type", "filePath", "relativePath"]) checkNoTrimString(issues, api[key], [index, key], 1);
30
- checkNoTrimString(issues, api.name, [index, "name"], 1, 100);
31
- if (!isString(api.fileName) || !isLowerCamelCaseName(api.fileName)) issues.push({ path: [index, "fileName"], message: "必须是 lowerCamelCase" });
32
- if (!isFunction(api.handler)) issues.push({ path: [index, "handler"], message: "必须是函数" });
33
- if (!isString(api.apiPath) || !apiPathRegex.test(api.apiPath)) issues.push({ path: [index, "apiPath"], message: "路径格式无效" });
34
- if (!["GET", "POST", "ALL"].includes(api.method)) issues.push({ path: [index, "method"], message: "仅允许 GET|POST|ALL" });
35
- if (!["none", "raw"].includes(api.body)) issues.push({ path: [index, "body"], message: "仅允许 none|raw" });
36
- // body 为 raw 时不允许配置 fields/required:parser 跳过解析,参数校验由 handler 自行承担
37
- if (api.body === "raw" && isPlainObject(api.fields) && Object.keys(api.fields).length > 0) {
38
- issues.push({ path: [index, "body"], message: 'body 为 "raw" 时不允许配置 fields,由 handler 自行解析校验' });
39
- }
40
- if (api.body === "raw" && Array.isArray(api.required) && api.required.length > 0) {
41
- issues.push({ path: [index, "body"], message: 'body 为 "raw" 时不允许配置 required,由 handler 自行解析校验' });
42
- }
43
-
44
- if (!isBoolean(api.auth) && (!Array.isArray(api.auth) || api.auth.length === 0 || api.auth.some((item) => !isString(item) || (item !== "" && item.trim() !== item)))) {
45
- issues.push({ path: [index, "auth"], message: "必须是 boolean 或非空字符串数组" });
46
- }
47
-
48
- if (!isPlainObject(api.fields)) {
49
- issues.push({ path: [index, "fields"], message: "必须是对象格式" });
50
- } else {
51
- for (const [fieldName, rule] of Object.entries(api.fields)) {
52
- if (!isLowerCamelCaseName(fieldName)) issues.push({ path: [index, "fields", fieldName], message: "字段名必须为 lowerCamelCase" });
53
- for (const issue of validateFieldRule(rule)) issues.push({ path: [index, "fields", fieldName, ...issue.path], message: issue.message });
1
+ import { Logger } from "#befly/libs/logger/index.js";
2
+ import { compile } from "#befly/libs/validator/index.js";
3
+
4
+ import apiSchema from "../schemas/api.json";
5
+ import { getFieldSchema } from "./field.js";
6
+ import { formatIssues } from "./validation.js";
7
+
8
+ const validate = compile(apiSchema);
9
+
10
+ function checkApiRules(issues, apis) {
11
+ apis.forEach((api, index) => {
12
+ if (!api || typeof api !== "object") return;
13
+ const fields = api.fields || {};
14
+ for (const [field, definition] of Object.entries(fields)) {
15
+ if (!definition || typeof definition !== "object" || Array.isArray(definition) || typeof definition.name !== "string" || definition.name.length === 0) {
16
+ issues.push({ path: [index, "fields", field], code: "rule", message: "必须提供非空 name" });
17
+ continue;
18
+ }
19
+ try {
20
+ compile(getFieldSchema(definition));
21
+ } catch (error) {
22
+ issues.push({ path: [index, "fields", field], code: "rule", message: error.message });
23
+ }
54
24
  }
55
- }
56
-
57
- if (!Array.isArray(api.required) || api.required.some((item) => !isString(item) || (item !== "" && item.trim() !== item))) issues.push({ path: [index, "required"], message: "必须是字符串数组" });
58
-
59
- return issues;
25
+ try {
26
+ compile({
27
+ paramType: "object",
28
+ fields: Object.fromEntries(Object.entries(fields).map(([field, definition]) => [field, { ...getFieldSchema(definition), optional: !(api.required || []).includes(field) }])),
29
+ constraints: api.constraints
30
+ });
31
+ } catch (error) {
32
+ issues.push({ path: [index, "constraints"], code: "rule", message: error.message });
33
+ }
34
+ for (const field of api.required || []) {
35
+ if (!Object.hasOwn(fields, field)) issues.push({ path: [index, "required"], code: "rule", message: `必填字段未在 fields 中声明: ${field}` });
36
+ else if (fields[field] && typeof fields[field] === "object" && Object.hasOwn(fields[field], "paramDefault")) issues.push({ path: [index, "required"], code: "rule", message: `必填字段不能同时配置 paramDefault: ${field}` });
37
+ }
38
+ if (api.body === "raw" && Object.keys(fields).length > 0) issues.push({ path: [index, "fields"], code: "rule", message: "body 为 raw 时不允许配置 fields" });
39
+ if (api.body === "raw" && (api.required || []).length > 0) issues.push({ path: [index, "required"], code: "rule", message: "body 为 raw 时不允许配置 required" });
40
+ });
60
41
  }
61
42
 
62
43
  export async function checkApi(apis) {
63
- const issues = [];
64
- if (!Array.isArray(apis)) {
65
- issues.push({ path: [], message: "必须是数组" });
66
- } else {
67
- apis.forEach((api, index) => issues.push(...checkApiItem(api, index)));
68
- }
69
-
44
+ const issues = validate(apis).issues;
45
+ if (Array.isArray(apis)) checkApiRules(issues, apis);
70
46
  if (issues.length === 0) return [];
71
- const errors = formatValidationIssues(issues, { items: apis, itemLabel: "api" });
47
+
48
+ const errors = formatIssues(issues, { items: apis, itemLabel: "api" });
72
49
  Logger.warn("接口校验失败", { errors: errors });
73
50
  return errors;
74
51
  }
package/checks/config.js CHANGED
@@ -1,18 +1,23 @@
1
- import { Logger } from "../lib/logger.js";
2
- import { validateConfig } from "../lib/validator.js";
3
- import { formatValidationIssues } from "../utils/formatValidationIssues.js";
1
+ import { Logger } from "#befly/libs/logger/index.js";
2
+ import { compile } from "#befly/libs/validator/index.js";
3
+
4
+ import configSchema from "../schemas/config.json";
5
+ import { formatIssues } from "./validation.js";
6
+
7
+ const validate = compile(configSchema);
4
8
 
5
9
  export async function checkConfig(config) {
6
- const issues = validateConfig(config);
10
+ const issues = validate(config).issues;
11
+ if (typeof config?.tz === "string") {
12
+ try {
13
+ new Intl.DateTimeFormat("en-US", { timeZone: config.tz }).format(0);
14
+ } catch {
15
+ issues.push({ path: ["tz"], code: "rule", message: "必须是有效 IANA 时区" });
16
+ }
17
+ }
7
18
  if (issues.length === 0) return [];
8
19
 
9
- const errors = formatValidationIssues(issues, { item: config, itemLabel: "config" });
10
- Logger.warn(
11
- "配置校验失败",
12
- {
13
- errors: errors
14
- },
15
- false
16
- );
20
+ const errors = formatIssues(issues, { item: config, itemLabel: "config" });
21
+ Logger.warn("配置校验失败", { errors: errors }, false);
17
22
  return errors;
18
23
  }
package/checks/corns.js CHANGED
@@ -1,54 +1,36 @@
1
- import { Logger } from "../lib/logger.js";
2
- import { formatValidationIssues } from "../utils/formatValidationIssues.js";
3
- import { isFunction, isPlainObject, isValidCronSchedule } from "../utils/is.js";
1
+ import { Logger } from "#befly/libs/logger/index.js";
2
+ import { compile } from "#befly/libs/validator/index.js";
4
3
 
5
- // scanFiles 基础字段 + 定时器业务字段(schedule/handler;时区统一走 config.tz)
6
- const cornKeys = new Set(["source", "type", "filePath", "relativePath", "fileName", "apiPath", "schedule", "handler"]);
4
+ import cornSchema from "../schemas/corn.json";
5
+ import { formatIssues } from "./validation.js";
7
6
 
8
- export const checkCorn = (corns) => {
9
- if (!Array.isArray(corns)) {
10
- const errors = formatValidationIssues([{ path: [], message: "必须是数组" }], { item: corns, itemLabel: "corn" });
11
- Logger.warn(`定时器校验失败`, { errors: errors });
12
- return errors;
13
- }
14
-
15
- const errors = [];
16
-
17
- // 任务名唯一性:文件名即任务名(core + app 合并后全局唯一,防止同名任务被 register 静默去重)
18
- const nameMap = new Map();
19
- for (const corn of corns) {
20
- if (!corn?.fileName) continue;
21
- nameMap.set(corn.fileName, (nameMap.get(corn.fileName) || 0) + 1);
22
- }
23
- const duplicateNames = [...nameMap].filter(([, count]) => count > 1).map(([name]) => name);
24
- if (duplicateNames.length > 0) {
25
- const duplicateCorns = corns.filter((corn) => duplicateNames.includes(corn.fileName));
26
- errors.push(
27
- ...formatValidationIssues(
28
- duplicateCorns.map((corn, index) => ({ path: [index], message: `任务名 ${corn.fileName} 重复,请显式区分` })),
29
- { items: duplicateCorns, itemLabel: "corn" }
30
- )
31
- );
32
- }
7
+ const validate = compile(cornSchema);
33
8
 
34
- for (const [index, corn] of corns.entries()) {
35
- if (!isPlainObject(corn)) {
36
- errors.push(...formatValidationIssues([{ path: [index], message: "必须是对象格式" }], { items: corns, itemLabel: "corn" }));
37
- continue;
38
- }
9
+ function checkCornNames(issues, corns) {
10
+ const counts = new Map();
11
+ for (const corn of corns) if (corn?.fileName) counts.set(corn.fileName, (counts.get(corn.fileName) || 0) + 1);
12
+ corns.forEach((corn, index) => {
13
+ if (corn?.fileName && counts.get(corn.fileName) > 1) issues.push({ path: [index], message: `任务名 ${corn.fileName} 重复,请显式区分` });
14
+ });
15
+ }
39
16
 
40
- const issues = [];
41
- for (const key of Object.keys(corn)) {
42
- if (!cornKeys.has(key)) issues.push({ path: [key], message: "不允许出现" });
43
- }
44
- if (!isFunction(corn.handler)) issues.push({ path: ["handler"], message: "必须是函数" });
45
- if (!isValidCronSchedule(corn.schedule)) issues.push({ path: ["schedule"], message: "必须是可注册的 cron 表达式" });
46
- if (issues.length === 0) continue;
47
- errors.push(...formatValidationIssues(issues, { items: corns, itemLabel: "corn" }));
17
+ export const checkCorn = (corns) => {
18
+ const issues = validate(corns).issues;
19
+ if (Array.isArray(corns)) {
20
+ checkCornNames(issues, corns);
21
+ corns.forEach((corn, index) => {
22
+ if (typeof corn?.schedule !== "string") return;
23
+ try {
24
+ const parsed = Bun.cron.parse(corn.schedule);
25
+ if (parsed === null) throw new TypeError("invalid cron");
26
+ } catch {
27
+ issues.push({ path: [index, "schedule"], code: "rule", message: "必须是可注册的 cron 表达式" });
28
+ }
29
+ });
48
30
  }
31
+ if (issues.length === 0) return [];
49
32
 
50
- if (errors.length > 0) {
51
- Logger.warn(`定时器校验失败`, { errors: errors });
52
- }
33
+ const errors = formatIssues(issues, { items: corns, itemLabel: "corn" });
34
+ Logger.warn("定时器校验失败", { errors: errors });
53
35
  return errors;
54
36
  };
@@ -0,0 +1,18 @@
1
+ const SCHEMA_KEYS = ["paramType", "anyOf", "optional", "paramDefault", "enum", "minValue", "maxValue", "minItem", "maxItem", "precision", "scale", "pattern", "unique", "uniqueBy", "items", "fields", "extra", "keys", "values", "constraints"];
2
+
3
+ /**
4
+ * 从 Core 字段定义中提取通用验证器协议,避免领域元数据进入验证器。
5
+ */
6
+ export function getFieldSchema(field) {
7
+ if (!field || typeof field !== "object" || Array.isArray(field)) return {};
8
+ return Object.fromEntries(SCHEMA_KEYS.filter((key) => field[key] !== undefined).map((key) => [key, field[key]]));
9
+ }
10
+
11
+ export function getFieldErrors(issues, fields) {
12
+ return Object.fromEntries(
13
+ issues.map((issue) => {
14
+ const field = issue.path[0];
15
+ return [field, `${fields[field]?.name || field}${issue.message}`];
16
+ })
17
+ );
18
+ }
package/checks/items.js CHANGED
@@ -1,67 +1,23 @@
1
- import { Logger } from "../lib/logger.js";
2
- import { formatValidationIssues } from "../utils/formatValidationIssues.js";
3
- import { isFunction, isFiniteNumber, isPlainObject, isString } from "../utils/is.js";
1
+ import { Logger } from "#befly/libs/logger/index.js";
2
+ import { compile } from "#befly/libs/validator/index.js";
4
3
 
5
- const itemKeys = new Set(["source", "type", "filePath", "relativePath", "fileName", "apiPath", "handler", "order"]);
4
+ import itemSchema from "../schemas/item.json";
5
+ import { formatIssues } from "./validation.js";
6
6
 
7
- function checkItem(item) {
8
- const issues = [];
9
- if (!isPlainObject(item)) {
10
- issues.push({ path: [], message: "必须是对象格式" });
11
- return issues;
12
- }
13
-
14
- for (const key of Object.keys(item)) {
15
- if (!itemKeys.has(key)) issues.push({ path: [key], message: "不允许出现" });
16
- }
17
- for (const key of ["source", "type", "filePath", "relativePath", "fileName", "apiPath"]) {
18
- if (!isString(item[key]) || item[key].length === 0 || item[key].trim() !== item[key]) issues.push({ path: [key], message: "必须是非空且无首尾空格的字符串" });
19
- }
20
- if (!isFunction(item.handler)) issues.push({ path: ["handler"], message: "必须是函数" });
21
- if (!isFiniteNumber(item.order)) issues.push({ path: ["order"], message: "必须是有限数字" });
22
- // 非核心(app)插件/钩子的 order 必须大于 1000,避免与核心区间(10~90)撞号
23
- if (item.source === "app" && isFiniteNumber(item.order) && item.order <= 1000) issues.push({ path: ["order"], message: "非核心插件/钩子 order 必须大于 1000" });
24
- return issues;
25
- }
26
-
27
- function checkItemList(items, label, labelEn) {
28
- if (!Array.isArray(items)) {
29
- const errors = formatValidationIssues([{ path: [], message: "必须是数组" }], { item: items, itemLabel: labelEn });
30
- Logger.warn(`${label}校验失败`, { errors: errors });
31
- return errors;
32
- }
7
+ const validate = compile(itemSchema);
33
8
 
34
- const errors = [];
35
-
36
- // order 冲突校验:同 order 的执行顺序依赖扫描顺序,要求显式区分
37
- const orderMap = new Map();
38
- for (const item of items) {
39
- if (!isFiniteNumber(item.order)) continue;
40
- orderMap.set(item.order, (orderMap.get(item.order) || 0) + 1);
41
- }
42
- const duplicateOrders = [...orderMap].filter(([, count]) => count > 1).map(([order]) => order);
43
- if (duplicateOrders.length > 0) {
44
- const duplicateItems = items.filter((item) => duplicateOrders.includes(item.order));
45
- errors.push(
46
- ...formatValidationIssues(
47
- duplicateItems.map((item, index) => ({ path: [index], message: `order ${item.order} 重复,请显式区分` })),
48
- { items: duplicateItems, itemLabel: labelEn }
49
- )
50
- );
51
- }
52
-
53
- for (const item of items) {
54
- const issues = checkItem(item);
55
- if (issues.length === 0) continue;
56
- errors.push(...formatValidationIssues(issues, { item: item, itemLabel: labelEn }));
57
- }
58
-
59
- if (errors.length > 0) {
60
- Logger.warn(`${label}校验失败`, { errors: errors });
9
+ function checkItems(items, label, labelEn) {
10
+ const issues = validate(items).issues;
11
+ if (Array.isArray(items)) {
12
+ items.forEach((item, index) => {
13
+ if (item?.source === "app" && typeof item.order === "number" && item.order <= 1000) issues.push({ path: [index, "order"], code: "rule", message: "非核心插件/钩子 order 必须大于 1000" });
14
+ });
61
15
  }
16
+ if (issues.length === 0) return [];
17
+ const errors = formatIssues(issues, { items: items, itemLabel: labelEn });
18
+ Logger.warn(`${label}校验失败`, { errors: errors });
62
19
  return errors;
63
20
  }
64
21
 
65
- export const checkHook = (hooks) => checkItemList(hooks, "钩子", "hook");
66
-
67
- export const checkPlugin = (plugins) => checkItemList(plugins, "插件", "plugin");
22
+ export const checkHook = (hooks) => checkItems(hooks, "钩子", "hook");
23
+ export const checkPlugin = (plugins) => checkItems(plugins, "插件", "plugin");