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
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* syncDb 常量模块测试
|
|
3
|
-
*
|
|
4
|
-
* 测试 constants.ts 中的常量:
|
|
5
|
-
* - DB_VERSION_REQUIREMENTS
|
|
6
|
-
* - SYSTEM_INDEX_FIELDS
|
|
7
|
-
* - SYSTEM_INDEX_FIELDS
|
|
8
|
-
* - CHANGE_TYPE_LABELS
|
|
9
|
-
* - MYSQL_TABLE_CONFIG
|
|
10
|
-
* - typeMapping
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { describe, test, expect, beforeAll } from "bun:test";
|
|
14
|
-
|
|
15
|
-
import { setDbType } from "../sync/syncDb/constants.js";
|
|
16
|
-
|
|
17
|
-
// 设置数据库类型为 MySQL
|
|
18
|
-
setDbType("mysql");
|
|
19
|
-
|
|
20
|
-
let constants: any;
|
|
21
|
-
|
|
22
|
-
beforeAll(async () => {
|
|
23
|
-
constants = await import("../sync/syncDb/constants.js");
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
describe("DB_VERSION_REQUIREMENTS", () => {
|
|
27
|
-
test("MySQL 最低版本为 8", () => {
|
|
28
|
-
expect(constants.DB_VERSION_REQUIREMENTS.MYSQL_MIN_MAJOR).toBe(8);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test("PostgreSQL 最低版本为 17", () => {
|
|
32
|
-
expect(constants.DB_VERSION_REQUIREMENTS.POSTGRES_MIN_MAJOR).toBe(17);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test("SQLite 最低版本为 3.50.0", () => {
|
|
36
|
-
expect(constants.DB_VERSION_REQUIREMENTS.SQLITE_MIN_VERSION).toBe("3.50.0");
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
describe("SYSTEM_INDEX_FIELDS", () => {
|
|
41
|
-
test("包含 created_at", () => {
|
|
42
|
-
expect(constants.SYSTEM_INDEX_FIELDS).toContain("created_at");
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test("包含 updated_at", () => {
|
|
46
|
-
expect(constants.SYSTEM_INDEX_FIELDS).toContain("updated_at");
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test("包含 state", () => {
|
|
50
|
-
expect(constants.SYSTEM_INDEX_FIELDS).toContain("state");
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test("共 3 个系统索引字段", () => {
|
|
54
|
-
expect(constants.SYSTEM_INDEX_FIELDS.length).toBe(3);
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
describe("CHANGE_TYPE_LABELS", () => {
|
|
59
|
-
test('length 对应 "长度"', () => {
|
|
60
|
-
expect(constants.CHANGE_TYPE_LABELS.length).toBe("长度");
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test('datatype 对应 "类型"', () => {
|
|
64
|
-
expect(constants.CHANGE_TYPE_LABELS.datatype).toBe("类型");
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test('comment 对应 "注释"', () => {
|
|
68
|
-
expect(constants.CHANGE_TYPE_LABELS.comment).toBe("注释");
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test('default 对应 "默认值"', () => {
|
|
72
|
-
expect(constants.CHANGE_TYPE_LABELS.default).toBe("默认值");
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
describe("MYSQL_TABLE_CONFIG", () => {
|
|
77
|
-
test("ENGINE 为 InnoDB", () => {
|
|
78
|
-
expect(constants.MYSQL_TABLE_CONFIG.ENGINE).toBe("InnoDB");
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
test("CHARSET 为 utf8mb4", () => {
|
|
82
|
-
expect(constants.MYSQL_TABLE_CONFIG.CHARSET).toBe("utf8mb4");
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
test("COLLATE 为 utf8mb4_0900_ai_ci", () => {
|
|
86
|
-
expect(constants.MYSQL_TABLE_CONFIG.COLLATE).toBe("utf8mb4_0900_ai_ci");
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
describe("getTypeMapping (MySQL)", () => {
|
|
91
|
-
test("number 映射为 BIGINT", () => {
|
|
92
|
-
expect(constants.getTypeMapping().number).toBe("BIGINT");
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
test("string 映射为 VARCHAR", () => {
|
|
96
|
-
expect(constants.getTypeMapping().string).toBe("VARCHAR");
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
test("text 映射为 MEDIUMTEXT", () => {
|
|
100
|
-
expect(constants.getTypeMapping().text).toBe("MEDIUMTEXT");
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
test("array_string 映射为 VARCHAR", () => {
|
|
104
|
-
expect(constants.getTypeMapping().array_string).toBe("VARCHAR");
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test("array_text 映射为 MEDIUMTEXT", () => {
|
|
108
|
-
expect(constants.getTypeMapping().array_text).toBe("MEDIUMTEXT");
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
describe("IS_PLAN", () => {
|
|
113
|
-
test("IS_PLAN 为 boolean 类型", () => {
|
|
114
|
-
expect(typeof constants.IS_PLAN).toBe("boolean");
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
describe("数据库类型判断 (MySQL)", () => {
|
|
119
|
-
test("isMySQL 为 true", () => {
|
|
120
|
-
expect(constants.isMySQL()).toBe(true);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
test("isPG 为 false", () => {
|
|
124
|
-
expect(constants.isPG()).toBe(false);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
test("isSQLite 为 false", () => {
|
|
128
|
-
expect(constants.isSQLite()).toBe(false);
|
|
129
|
-
});
|
|
130
|
-
});
|
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* syncDb 表结构查询模块测试
|
|
3
|
-
*
|
|
4
|
-
* 测试 schema.ts 中的函数(纯逻辑测试,不需要数据库连接):
|
|
5
|
-
* - tableExists
|
|
6
|
-
* - getTableColumns
|
|
7
|
-
* - getTableIndexes
|
|
8
|
-
*
|
|
9
|
-
* 注意:这些是模拟测试,实际数据库操作需要集成测试
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { describe, test, expect, beforeAll } from "bun:test";
|
|
13
|
-
|
|
14
|
-
import { setDbType } from "../sync/syncDb/constants.js";
|
|
15
|
-
|
|
16
|
-
// 设置数据库类型为 MySQL
|
|
17
|
-
setDbType("mysql");
|
|
18
|
-
|
|
19
|
-
let tableExists: any;
|
|
20
|
-
let getTableColumns: any;
|
|
21
|
-
let getTableIndexes: any;
|
|
22
|
-
|
|
23
|
-
beforeAll(async () => {
|
|
24
|
-
const schema = await import("../sync/syncDb/schema.js");
|
|
25
|
-
tableExists = schema.tableExists;
|
|
26
|
-
getTableColumns = schema.getTableColumns;
|
|
27
|
-
getTableIndexes = schema.getTableIndexes;
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
describe("tableExists", () => {
|
|
31
|
-
test("sql 客户端未初始化时抛出错误", async () => {
|
|
32
|
-
try {
|
|
33
|
-
await tableExists(null, "user");
|
|
34
|
-
expect(true).toBe(false); // 不应该到这里
|
|
35
|
-
} catch (error: any) {
|
|
36
|
-
expect(error.message).toBe("SQL 客户端未初始化");
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test("传入有效 sql 客户端时正常执行", async () => {
|
|
41
|
-
// 创建模拟 SQL 客户端
|
|
42
|
-
const mockSql = Object.assign(
|
|
43
|
-
async function (_strings: TemplateStringsArray, ..._values: any[]) {
|
|
44
|
-
// 模拟 MySQL 查询返回
|
|
45
|
-
return [{ count: 1 }];
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
unsafe: async (_query: string) => []
|
|
49
|
-
}
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
const result = await tableExists(mockSql, "user", "test_db");
|
|
53
|
-
expect(result).toBe(true);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test("表不存在时返回 false", async () => {
|
|
57
|
-
const mockSql = Object.assign(
|
|
58
|
-
async function (_strings: TemplateStringsArray, ..._values: any[]) {
|
|
59
|
-
return [{ count: 0 }];
|
|
60
|
-
},
|
|
61
|
-
{
|
|
62
|
-
unsafe: async (_query: string) => []
|
|
63
|
-
}
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
const result = await tableExists(mockSql, "nonexistent", "test_db");
|
|
67
|
-
expect(result).toBe(false);
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
describe("getTableColumns", () => {
|
|
72
|
-
test("返回正确的列信息结构", async () => {
|
|
73
|
-
const mockSql = Object.assign(
|
|
74
|
-
async function (_strings: TemplateStringsArray, ..._values: any[]) {
|
|
75
|
-
// 模拟 MySQL information_schema 返回
|
|
76
|
-
return [
|
|
77
|
-
{
|
|
78
|
-
COLUMN_NAME: "id",
|
|
79
|
-
DATA_TYPE: "bigint",
|
|
80
|
-
CHARACTER_MAXIMUM_LENGTH: null,
|
|
81
|
-
IS_NULLABLE: "NO",
|
|
82
|
-
COLUMN_DEFAULT: null,
|
|
83
|
-
COLUMN_COMMENT: "主键ID",
|
|
84
|
-
COLUMN_TYPE: "bigint unsigned"
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
COLUMN_NAME: "user_name",
|
|
88
|
-
DATA_TYPE: "varchar",
|
|
89
|
-
CHARACTER_MAXIMUM_LENGTH: 50,
|
|
90
|
-
IS_NULLABLE: "NO",
|
|
91
|
-
COLUMN_DEFAULT: "",
|
|
92
|
-
COLUMN_COMMENT: "用户名",
|
|
93
|
-
COLUMN_TYPE: "varchar(50)"
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
COLUMN_NAME: "age",
|
|
97
|
-
DATA_TYPE: "bigint",
|
|
98
|
-
CHARACTER_MAXIMUM_LENGTH: null,
|
|
99
|
-
IS_NULLABLE: "YES",
|
|
100
|
-
COLUMN_DEFAULT: "0",
|
|
101
|
-
COLUMN_COMMENT: "年龄",
|
|
102
|
-
COLUMN_TYPE: "bigint"
|
|
103
|
-
}
|
|
104
|
-
];
|
|
105
|
-
},
|
|
106
|
-
{
|
|
107
|
-
unsafe: async (_query: string) => []
|
|
108
|
-
}
|
|
109
|
-
);
|
|
110
|
-
|
|
111
|
-
const columns = await getTableColumns(mockSql, "user", "test_db");
|
|
112
|
-
|
|
113
|
-
expect(columns.id).toBeDefined();
|
|
114
|
-
expect(columns.id.type).toBe("bigint");
|
|
115
|
-
expect(columns.id.nullable).toBe(false);
|
|
116
|
-
expect(columns.id.comment).toBe("主键ID");
|
|
117
|
-
|
|
118
|
-
expect(columns.user_name).toBeDefined();
|
|
119
|
-
expect(columns.user_name.type).toBe("varchar");
|
|
120
|
-
expect(columns.user_name.max).toBe(50);
|
|
121
|
-
expect(columns.user_name.nullable).toBe(false);
|
|
122
|
-
expect(columns.user_name.defaultValue).toBe("");
|
|
123
|
-
|
|
124
|
-
expect(columns.age).toBeDefined();
|
|
125
|
-
expect(columns.age.nullable).toBe(true);
|
|
126
|
-
expect(columns.age.defaultValue).toBe("0");
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
describe("getTableIndexes", () => {
|
|
131
|
-
test("返回正确的索引信息结构", async () => {
|
|
132
|
-
const mockSql = Object.assign(
|
|
133
|
-
async function (_strings: TemplateStringsArray, ..._values: any[]) {
|
|
134
|
-
// 模拟 MySQL information_schema.STATISTICS 返回
|
|
135
|
-
// 注意:PRIMARY 索引被排除
|
|
136
|
-
return [
|
|
137
|
-
{ INDEX_NAME: "idx_created_at", COLUMN_NAME: "created_at" },
|
|
138
|
-
{ INDEX_NAME: "idx_user_name", COLUMN_NAME: "user_name" }
|
|
139
|
-
];
|
|
140
|
-
},
|
|
141
|
-
{
|
|
142
|
-
unsafe: async (_query: string) => []
|
|
143
|
-
}
|
|
144
|
-
);
|
|
145
|
-
|
|
146
|
-
const indexes = await getTableIndexes(mockSql, "user", "test_db");
|
|
147
|
-
|
|
148
|
-
// PRIMARY 索引被排除,不应存在
|
|
149
|
-
expect(indexes.PRIMARY).toBeUndefined();
|
|
150
|
-
|
|
151
|
-
expect(indexes.idx_created_at).toBeDefined();
|
|
152
|
-
expect(indexes.idx_created_at).toContain("created_at");
|
|
153
|
-
|
|
154
|
-
expect(indexes.idx_user_name).toBeDefined();
|
|
155
|
-
expect(indexes.idx_user_name).toContain("user_name");
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
test("复合索引包含多个列", async () => {
|
|
159
|
-
const mockSql = Object.assign(
|
|
160
|
-
async function (_strings: TemplateStringsArray, ..._values: any[]) {
|
|
161
|
-
// 模拟复合索引,同一索引名包含多个列
|
|
162
|
-
return [
|
|
163
|
-
{ INDEX_NAME: "idx_composite", COLUMN_NAME: "user_id" },
|
|
164
|
-
{ INDEX_NAME: "idx_composite", COLUMN_NAME: "created_at" }
|
|
165
|
-
];
|
|
166
|
-
},
|
|
167
|
-
{
|
|
168
|
-
unsafe: async (_query: string) => []
|
|
169
|
-
}
|
|
170
|
-
);
|
|
171
|
-
|
|
172
|
-
const indexes = await getTableIndexes(mockSql, "user", "test_db");
|
|
173
|
-
|
|
174
|
-
expect(indexes.idx_composite).toBeDefined();
|
|
175
|
-
expect(indexes.idx_composite.length).toBe(2);
|
|
176
|
-
expect(indexes.idx_composite).toContain("user_id");
|
|
177
|
-
expect(indexes.idx_composite).toContain("created_at");
|
|
178
|
-
});
|
|
179
|
-
});
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* syncDb 类型处理模块测试
|
|
3
|
-
*
|
|
4
|
-
* 测试 types.ts 中的函数:
|
|
5
|
-
* - isStringOrArrayType
|
|
6
|
-
* - getSqlType
|
|
7
|
-
* - resolveDefaultValue
|
|
8
|
-
* - generateDefaultSql
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { describe, test, expect, beforeAll } from "bun:test";
|
|
12
|
-
|
|
13
|
-
import { setDbType } from "../sync/syncDb/constants.js";
|
|
14
|
-
|
|
15
|
-
// 设置数据库类型为 MySQL
|
|
16
|
-
setDbType("mysql");
|
|
17
|
-
|
|
18
|
-
// 动态导入以确保环境变量生效
|
|
19
|
-
let isStringOrArrayType: any;
|
|
20
|
-
let getSqlType: any;
|
|
21
|
-
let resolveDefaultValue: any;
|
|
22
|
-
let generateDefaultSql: any;
|
|
23
|
-
|
|
24
|
-
beforeAll(async () => {
|
|
25
|
-
const types = await import("../sync/syncDb/types.js");
|
|
26
|
-
isStringOrArrayType = types.isStringOrArrayType;
|
|
27
|
-
getSqlType = types.getSqlType;
|
|
28
|
-
resolveDefaultValue = types.resolveDefaultValue;
|
|
29
|
-
generateDefaultSql = types.generateDefaultSql;
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
describe("isStringOrArrayType", () => {
|
|
33
|
-
test("string 类型返回 true", () => {
|
|
34
|
-
expect(isStringOrArrayType("string")).toBe(true);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
test("array_string 类型返回 true", () => {
|
|
38
|
-
expect(isStringOrArrayType("array_string")).toBe(true);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test("number 类型返回 false", () => {
|
|
42
|
-
expect(isStringOrArrayType("number")).toBe(false);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test("text 类型返回 false", () => {
|
|
46
|
-
expect(isStringOrArrayType("text")).toBe(false);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test("array_text 类型返回 false", () => {
|
|
50
|
-
expect(isStringOrArrayType("array_text")).toBe(false);
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
describe("resolveDefaultValue", () => {
|
|
55
|
-
test("null 值 + string 类型 => 空字符串", () => {
|
|
56
|
-
expect(resolveDefaultValue(null, "string")).toBe("");
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test("null 值 + number 类型 => 0", () => {
|
|
60
|
-
expect(resolveDefaultValue(null, "number")).toBe(0);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test('"null" 字符串 + number 类型 => 0', () => {
|
|
64
|
-
expect(resolveDefaultValue("null", "number")).toBe(0);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test('null 值 + array_string 类型 => "[]"', () => {
|
|
68
|
-
expect(resolveDefaultValue(null, "array_string")).toBe("[]");
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test('null 值 + text 类型 => "null"', () => {
|
|
72
|
-
expect(resolveDefaultValue(null, "text")).toBe("null");
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
test('null 值 + array_text 类型 => "null"(TEXT 不支持默认值)', () => {
|
|
76
|
-
expect(resolveDefaultValue(null, "array_text")).toBe("null");
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test("有实际值时直接返回", () => {
|
|
80
|
-
expect(resolveDefaultValue("admin", "string")).toBe("admin");
|
|
81
|
-
expect(resolveDefaultValue(100, "number")).toBe(100);
|
|
82
|
-
expect(resolveDefaultValue(0, "number")).toBe(0);
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
describe("generateDefaultSql", () => {
|
|
87
|
-
test("number 类型生成数字默认值", () => {
|
|
88
|
-
expect(generateDefaultSql(0, "number")).toBe(" DEFAULT 0");
|
|
89
|
-
expect(generateDefaultSql(100, "number")).toBe(" DEFAULT 100");
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
test("string 类型生成带引号默认值", () => {
|
|
93
|
-
expect(generateDefaultSql("admin", "string")).toBe(" DEFAULT 'admin'");
|
|
94
|
-
expect(generateDefaultSql("", "string")).toBe(" DEFAULT ''");
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
test("text 类型不生成默认值", () => {
|
|
98
|
-
expect(generateDefaultSql("null", "text")).toBe("");
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
test("array_string 类型生成 JSON 数组默认值", () => {
|
|
102
|
-
expect(generateDefaultSql("[]", "array_string")).toBe(" DEFAULT '[]'");
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
test("array_text 类型不生成默认值(MySQL TEXT 不支持)", () => {
|
|
106
|
-
expect(generateDefaultSql("[]", "array_text")).toBe("");
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
test("单引号被正确转义", () => {
|
|
110
|
-
expect(generateDefaultSql("it's", "string")).toBe(" DEFAULT 'it''s'");
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
describe("getSqlType", () => {
|
|
115
|
-
test("string 类型带长度", () => {
|
|
116
|
-
const result = getSqlType("string", 100);
|
|
117
|
-
expect(result).toBe("VARCHAR(100)");
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
test("array_string 类型带长度", () => {
|
|
121
|
-
const result = getSqlType("array_string", 500);
|
|
122
|
-
expect(result).toBe("VARCHAR(500)");
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
test("number 类型无符号", () => {
|
|
126
|
-
const result = getSqlType("number", null, true);
|
|
127
|
-
expect(result).toBe("BIGINT UNSIGNED");
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
test("number 类型有符号", () => {
|
|
131
|
-
const result = getSqlType("number", null, false);
|
|
132
|
-
expect(result).toBe("BIGINT");
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
test("text 类型", () => {
|
|
136
|
-
const result = getSqlType("text", null);
|
|
137
|
-
expect(result).toBe("MEDIUMTEXT");
|
|
138
|
-
});
|
|
139
|
-
});
|
package/utils/addonHelper.ts
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import { existsSync, statSync } from "node:fs";
|
|
3
|
-
|
|
4
|
-
import { join } from "pathe";
|
|
5
|
-
|
|
6
|
-
import { projectAddonsDir } from "../paths.js";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* 扫描所有可用的 addon
|
|
10
|
-
* 优先从本地 addons/ 目录加载,其次从 node_modules/@befly-addon/ 加载
|
|
11
|
-
* @param cwd - 项目根目录,默认为 process.cwd()
|
|
12
|
-
* @returns addon 名称数组
|
|
13
|
-
*/
|
|
14
|
-
export const scanAddons = (cwd: string = process.cwd()): string[] => {
|
|
15
|
-
const addons = new Set<string>();
|
|
16
|
-
|
|
17
|
-
// 1. 扫描本地 addons 目录(优先级高)
|
|
18
|
-
if (existsSync(projectAddonsDir)) {
|
|
19
|
-
try {
|
|
20
|
-
const localAddons = fs.readdirSync(projectAddonsDir).filter((name) => {
|
|
21
|
-
const fullPath = join(projectAddonsDir, name);
|
|
22
|
-
try {
|
|
23
|
-
const stat = statSync(fullPath);
|
|
24
|
-
return stat.isDirectory() && !name.startsWith("_");
|
|
25
|
-
} catch {
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
localAddons.forEach((name) => addons.add(name));
|
|
30
|
-
} catch {
|
|
31
|
-
// 忽略本地目录读取错误
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// 2. 扫描 node_modules/@befly-addon 目录
|
|
36
|
-
const beflyDir = join(cwd, "node_modules", "@befly-addon");
|
|
37
|
-
if (existsSync(beflyDir)) {
|
|
38
|
-
try {
|
|
39
|
-
const npmAddons = fs.readdirSync(beflyDir).filter((name) => {
|
|
40
|
-
// 如果本地已存在,跳过 npm 包版本
|
|
41
|
-
if (addons.has(name)) return false;
|
|
42
|
-
|
|
43
|
-
const fullPath = join(beflyDir, name);
|
|
44
|
-
try {
|
|
45
|
-
const stat = statSync(fullPath);
|
|
46
|
-
return stat.isDirectory();
|
|
47
|
-
} catch {
|
|
48
|
-
return false;
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
npmAddons.forEach((name) => addons.add(name));
|
|
52
|
-
} catch {
|
|
53
|
-
// 忽略 npm 目录读取错误
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return Array.from(addons).sort();
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* 获取 addon 的指定子目录路径
|
|
62
|
-
* 优先返回本地 addons 目录,其次返回 node_modules 目录
|
|
63
|
-
* @param name - addon 名称
|
|
64
|
-
* @param subDir - 子目录名称
|
|
65
|
-
* @param cwd - 项目根目录,默认为 process.cwd()
|
|
66
|
-
* @returns 完整路径
|
|
67
|
-
*/
|
|
68
|
-
export const getAddonDir = (name: string, subDir: string, cwd: string = process.cwd()): string => {
|
|
69
|
-
// 优先使用本地 addons 目录
|
|
70
|
-
// const projectAddonsDir = join(cwd, 'addons');
|
|
71
|
-
// const localPath = join(projectAddonsDir, name, subDir);
|
|
72
|
-
// if (existsSync(localPath)) {
|
|
73
|
-
// return localPath;
|
|
74
|
-
// }
|
|
75
|
-
|
|
76
|
-
// 降级使用 node_modules 目录
|
|
77
|
-
return join(cwd, "node_modules", "@befly-addon", name, subDir);
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* 检查 addon 子目录是否存在
|
|
82
|
-
* @param name - addon 名称
|
|
83
|
-
* @param subDir - 子目录名称
|
|
84
|
-
* @param cwd - 项目根目录,默认为 process.cwd()
|
|
85
|
-
* @returns 是否存在
|
|
86
|
-
*/
|
|
87
|
-
export const addonDirExists = (name: string, subDir: string, cwd: string = process.cwd()): boolean => {
|
|
88
|
-
const dir = getAddonDir(name, subDir, cwd);
|
|
89
|
-
return existsSync(dir) && statSync(dir).isDirectory();
|
|
90
|
-
};
|
package/utils/modules.ts
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import type { Hook } from "../types/hook.js";
|
|
2
|
-
import type { Plugin } from "../types/plugin.js";
|
|
3
|
-
|
|
4
|
-
import { existsSync } from "node:fs";
|
|
5
|
-
|
|
6
|
-
import { camelCase } from "es-toolkit/string";
|
|
7
|
-
|
|
8
|
-
import { Logger } from "../lib/logger.js";
|
|
9
|
-
import { scanFiles } from "./scanFiles.js";
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* 扫描模块(插件或钩子)
|
|
13
|
-
* @param dir - 目录路径
|
|
14
|
-
* @param type - 模块类型(core/addon/app)
|
|
15
|
-
* @param moduleLabel - 模块标签(如"插件"、"钩子")
|
|
16
|
-
* @param addonName - 组件名称(仅 type='addon' 时需要)
|
|
17
|
-
* @returns 模块列表
|
|
18
|
-
*/
|
|
19
|
-
export async function scanModules<T extends Plugin | Hook>(dir: string, type: "core" | "addon" | "app", moduleLabel: string, addonName?: string): Promise<T[]> {
|
|
20
|
-
if (!existsSync(dir)) return [];
|
|
21
|
-
|
|
22
|
-
const items: T[] = [];
|
|
23
|
-
const files = await scanFiles(dir, "*.ts");
|
|
24
|
-
|
|
25
|
-
for (const { filePath, fileName } of files) {
|
|
26
|
-
// 生成模块名称
|
|
27
|
-
const name = camelCase(fileName);
|
|
28
|
-
const moduleName = type === "core" ? name : type === "addon" ? `addon_${camelCase(addonName!)}_${name}` : `app_${name}`;
|
|
29
|
-
|
|
30
|
-
try {
|
|
31
|
-
const normalizedFilePath = filePath.replace(/\\/g, "/");
|
|
32
|
-
const moduleImport = await import(normalizedFilePath);
|
|
33
|
-
const item = moduleImport.default;
|
|
34
|
-
|
|
35
|
-
item.name = moduleName;
|
|
36
|
-
// 为 addon 模块记录 addon 名称
|
|
37
|
-
if (type === "addon" && addonName) {
|
|
38
|
-
item.addonName = addonName;
|
|
39
|
-
}
|
|
40
|
-
items.push(item);
|
|
41
|
-
} catch (err: any) {
|
|
42
|
-
const typeLabel = type === "core" ? "核心" : type === "addon" ? `组件${addonName}` : "项目";
|
|
43
|
-
Logger.error({ err: err, module: fileName }, `${typeLabel}${moduleLabel} 导入失败`);
|
|
44
|
-
process.exit(1);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return items;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* 排序模块(根据依赖关系)
|
|
53
|
-
* @param modules - 待排序的模块列表
|
|
54
|
-
* @returns 排序后的模块列表,如果存在循环依赖或依赖不存在则返回 false
|
|
55
|
-
*/
|
|
56
|
-
export function sortModules<T extends { name?: string; after?: string[] }>(modules: T[]): T[] | false {
|
|
57
|
-
const result: T[] = [];
|
|
58
|
-
const visited = new Set<string>();
|
|
59
|
-
const visiting = new Set<string>();
|
|
60
|
-
const moduleMap: Record<string, T> = Object.fromEntries(modules.map((m) => [m.name!, m]));
|
|
61
|
-
let isPass = true;
|
|
62
|
-
|
|
63
|
-
// 检查依赖是否存在
|
|
64
|
-
for (const module of modules) {
|
|
65
|
-
if (module.after) {
|
|
66
|
-
for (const dep of module.after) {
|
|
67
|
-
if (!moduleMap[dep]) {
|
|
68
|
-
Logger.error({ module: module.name, dependency: dep }, "依赖的模块未找到");
|
|
69
|
-
isPass = false;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (!isPass) return false;
|
|
76
|
-
|
|
77
|
-
const visit = (name: string): void => {
|
|
78
|
-
if (visited.has(name)) return;
|
|
79
|
-
if (visiting.has(name)) {
|
|
80
|
-
Logger.error({ module: name }, "模块循环依赖");
|
|
81
|
-
isPass = false;
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const module = moduleMap[name];
|
|
86
|
-
if (!module) return;
|
|
87
|
-
|
|
88
|
-
visiting.add(name);
|
|
89
|
-
(module.after || []).forEach(visit);
|
|
90
|
-
visiting.delete(name);
|
|
91
|
-
visited.add(name);
|
|
92
|
-
result.push(module);
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
modules.forEach((m) => visit(m.name!));
|
|
96
|
-
|
|
97
|
-
return isPass ? result : false;
|
|
98
|
-
}
|
package/utils/route.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 标准化接口权限路径(用于写入/读取权限缓存时保持一致)
|
|
3
|
-
* 规则(简化版):method 大写;path 为空则为 '/';确保以 '/' 开头
|
|
4
|
-
* 说明:当前框架内 api.path 来源于目录结构生成、请求侧使用 URL.pathname,默认不包含 query/hash,且不期望出现尾部斜杠等异常输入。
|
|
5
|
-
*/
|
|
6
|
-
export function normalizeApiPath(method: string, path: string): string {
|
|
7
|
-
const normalizedMethod = (method || "").toUpperCase();
|
|
8
|
-
let normalizedPath = path || "/";
|
|
9
|
-
|
|
10
|
-
if (!normalizedPath.startsWith("/")) {
|
|
11
|
-
normalizedPath = "/" + normalizedPath;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
return `${normalizedMethod}${normalizedPath}`;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* 生成路由 Key(用于路由匹配 / 权限缓存 / 日志等场景统一使用)
|
|
19
|
-
* 格式:METHOD/path
|
|
20
|
-
*/
|
|
21
|
-
export function makeRouteKey(method: string, pathname: string): string {
|
|
22
|
-
return normalizeApiPath(method, pathname);
|
|
23
|
-
}
|