befly 3.9.38 → 3.9.39
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 +37 -38
- package/befly.config.ts +62 -40
- package/checks/checkApi.ts +16 -16
- package/checks/checkApp.ts +19 -25
- package/checks/checkTable.ts +42 -42
- package/docs/README.md +42 -35
- package/docs/{api.md → api/api.md} +223 -231
- package/docs/cipher.md +71 -69
- package/docs/database.md +143 -141
- package/docs/{examples.md → guide/examples.md} +181 -181
- package/docs/guide/quickstart.md +331 -0
- package/docs/hooks/auth.md +38 -0
- package/docs/hooks/cors.md +28 -0
- package/docs/{hook.md → hooks/hook.md} +140 -57
- package/docs/hooks/parser.md +19 -0
- package/docs/hooks/rateLimit.md +47 -0
- package/docs/{redis.md → infra/redis.md} +84 -93
- package/docs/plugins/cipher.md +61 -0
- package/docs/plugins/database.md +128 -0
- package/docs/{plugin.md → plugins/plugin.md} +83 -81
- package/docs/quickstart.md +26 -26
- package/docs/{addon.md → reference/addon.md} +46 -46
- package/docs/{config.md → reference/config.md} +32 -80
- package/docs/{logger.md → reference/logger.md} +52 -52
- package/docs/{sync.md → reference/sync.md} +32 -35
- package/docs/{table.md → reference/table.md} +1 -1
- package/docs/{validator.md → reference/validator.md} +57 -57
- package/hooks/auth.ts +8 -4
- package/hooks/cors.ts +13 -13
- package/hooks/parser.ts +37 -17
- package/hooks/permission.ts +26 -14
- package/hooks/rateLimit.ts +276 -0
- package/hooks/validator.ts +7 -7
- package/lib/asyncContext.ts +43 -0
- package/lib/cacheHelper.ts +212 -77
- package/lib/cacheKeys.ts +38 -0
- package/lib/cipher.ts +30 -30
- package/lib/connect.ts +28 -28
- package/lib/dbHelper.ts +183 -102
- package/lib/jwt.ts +16 -16
- package/lib/logger.ts +610 -19
- package/lib/redisHelper.ts +185 -44
- package/lib/sqlBuilder.ts +90 -91
- package/lib/validator.ts +59 -39
- package/loader/loadApis.ts +48 -44
- package/loader/loadHooks.ts +40 -14
- package/loader/loadPlugins.ts +16 -17
- package/main.ts +57 -47
- package/package.json +47 -45
- package/paths.ts +15 -14
- package/plugins/cache.ts +5 -4
- package/plugins/cipher.ts +3 -3
- package/plugins/config.ts +2 -2
- package/plugins/db.ts +9 -9
- package/plugins/jwt.ts +3 -3
- package/plugins/logger.ts +8 -12
- package/plugins/redis.ts +8 -8
- package/plugins/tool.ts +6 -6
- package/router/api.ts +85 -56
- package/router/static.ts +12 -12
- package/sync/syncAll.ts +12 -12
- package/sync/syncApi.ts +55 -52
- package/sync/syncDb/apply.ts +20 -19
- package/sync/syncDb/constants.ts +25 -23
- package/sync/syncDb/ddl.ts +35 -36
- package/sync/syncDb/helpers.ts +6 -9
- package/sync/syncDb/schema.ts +10 -9
- package/sync/syncDb/sqlite.ts +7 -8
- package/sync/syncDb/table.ts +37 -35
- package/sync/syncDb/tableCreate.ts +21 -20
- package/sync/syncDb/types.ts +23 -20
- package/sync/syncDb/version.ts +10 -10
- package/sync/syncDb.ts +43 -36
- package/sync/syncDev.ts +74 -65
- package/sync/syncMenu.ts +190 -55
- package/tests/api-integration-array-number.test.ts +282 -0
- package/tests/befly-config-env.test.ts +78 -0
- package/tests/cacheHelper.test.ts +135 -104
- package/tests/cacheKeys.test.ts +41 -0
- package/tests/cipher.test.ts +90 -89
- package/tests/dbHelper-advanced.test.ts +140 -134
- package/tests/dbHelper-all-array-types.test.ts +316 -0
- package/tests/dbHelper-array-serialization.test.ts +258 -0
- package/tests/dbHelper-columns.test.ts +56 -55
- package/tests/dbHelper-execute.test.ts +45 -44
- package/tests/dbHelper-joins.test.ts +124 -119
- package/tests/fields-redis-cache.test.ts +29 -27
- package/tests/fields-validate.test.ts +38 -38
- package/tests/getClientIp.test.ts +54 -0
- package/tests/integration.test.ts +69 -67
- package/tests/jwt.test.ts +27 -26
- package/tests/logger.test.ts +267 -34
- package/tests/rateLimit-hook.test.ts +477 -0
- package/tests/redisHelper.test.ts +187 -188
- package/tests/redisKeys.test.ts +6 -73
- package/tests/scanConfig.test.ts +144 -0
- package/tests/sqlBuilder-advanced.test.ts +217 -215
- package/tests/sqlBuilder.test.ts +92 -91
- package/tests/sync-connection.test.ts +29 -29
- package/tests/syncDb-apply.test.ts +97 -96
- package/tests/syncDb-array-number.test.ts +160 -0
- package/tests/syncDb-constants.test.ts +48 -47
- package/tests/syncDb-ddl.test.ts +99 -98
- package/tests/syncDb-helpers.test.ts +29 -28
- package/tests/syncDb-schema.test.ts +61 -60
- package/tests/syncDb-types.test.ts +60 -59
- package/tests/syncMenu-paths.test.ts +68 -0
- package/tests/util.test.ts +42 -41
- package/tests/validator-array-number.test.ts +310 -0
- package/tests/validator-default.test.ts +373 -0
- package/tests/validator.test.ts +271 -266
- package/tsconfig.json +4 -5
- package/types/api.d.ts +7 -12
- package/types/befly.d.ts +60 -13
- package/types/cache.d.ts +8 -4
- package/types/common.d.ts +17 -9
- package/types/context.d.ts +2 -2
- package/types/crypto.d.ts +23 -0
- package/types/database.d.ts +19 -19
- package/types/hook.d.ts +2 -2
- package/types/jwt.d.ts +118 -0
- package/types/logger.d.ts +30 -0
- package/types/plugin.d.ts +4 -4
- package/types/redis.d.ts +7 -3
- package/types/roleApisCache.ts +23 -0
- package/types/sync.d.ts +10 -10
- package/types/table.d.ts +50 -9
- package/types/validate.d.ts +69 -0
- package/utils/addonHelper.ts +90 -0
- package/utils/arrayKeysToCamel.ts +18 -0
- package/utils/calcPerfTime.ts +13 -0
- package/utils/configTypes.ts +3 -0
- package/utils/cors.ts +19 -0
- package/utils/fieldClear.ts +75 -0
- package/utils/genShortId.ts +12 -0
- package/utils/getClientIp.ts +45 -0
- package/utils/keysToCamel.ts +22 -0
- package/utils/keysToSnake.ts +22 -0
- package/utils/modules.ts +98 -0
- package/utils/pickFields.ts +19 -0
- package/utils/process.ts +56 -0
- package/utils/regex.ts +225 -0
- package/utils/response.ts +115 -0
- package/utils/route.ts +23 -0
- package/utils/scanConfig.ts +142 -0
- package/utils/scanFiles.ts +48 -0
- package/.prettierignore +0 -2
- package/.prettierrc +0 -12
- package/docs/1-/345/237/272/346/234/254/344/273/213/347/273/215.md +0 -35
- package/docs/2-/345/210/235/346/255/245/344/275/223/351/252/214.md +0 -64
- package/docs/3-/347/254/254/344/270/200/344/270/252/346/216/245/345/217/243.md +0 -46
- package/docs/4-/346/223/215/344/275/234/346/225/260/346/215/256/345/272/223.md +0 -172
- package/hooks/requestLogger.ts +0 -84
- package/types/index.ts +0 -24
- package/util.ts +0 -283
|
@@ -9,170 +9,171 @@
|
|
|
9
9
|
* 注意:这些是模拟测试,实际数据库操作需要集成测试
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { describe, test, expect, beforeAll
|
|
13
|
-
|
|
12
|
+
import { describe, test, expect, beforeAll } from "bun:test";
|
|
13
|
+
|
|
14
|
+
import { setDbType } from "../sync/syncDb/constants.js";
|
|
14
15
|
|
|
15
16
|
// 设置数据库类型为 MySQL
|
|
16
|
-
setDbType(
|
|
17
|
+
setDbType("mysql");
|
|
17
18
|
|
|
18
19
|
let tableExists: any;
|
|
19
20
|
let getTableColumns: any;
|
|
20
21
|
let getTableIndexes: any;
|
|
21
22
|
|
|
22
23
|
beforeAll(async () => {
|
|
23
|
-
const schema = await import(
|
|
24
|
+
const schema = await import("../sync/syncDb/schema.js");
|
|
24
25
|
tableExists = schema.tableExists;
|
|
25
26
|
getTableColumns = schema.getTableColumns;
|
|
26
27
|
getTableIndexes = schema.getTableIndexes;
|
|
27
28
|
});
|
|
28
29
|
|
|
29
|
-
describe(
|
|
30
|
-
test(
|
|
30
|
+
describe("tableExists", () => {
|
|
31
|
+
test("sql 客户端未初始化时抛出错误", async () => {
|
|
31
32
|
try {
|
|
32
|
-
await tableExists(null,
|
|
33
|
+
await tableExists(null, "user");
|
|
33
34
|
expect(true).toBe(false); // 不应该到这里
|
|
34
35
|
} catch (error: any) {
|
|
35
|
-
expect(error.message).toBe(
|
|
36
|
+
expect(error.message).toBe("SQL 客户端未初始化");
|
|
36
37
|
}
|
|
37
38
|
});
|
|
38
39
|
|
|
39
|
-
test(
|
|
40
|
+
test("传入有效 sql 客户端时正常执行", async () => {
|
|
40
41
|
// 创建模拟 SQL 客户端
|
|
41
42
|
const mockSql = Object.assign(
|
|
42
|
-
async function (
|
|
43
|
+
async function (_strings: TemplateStringsArray, ..._values: any[]) {
|
|
43
44
|
// 模拟 MySQL 查询返回
|
|
44
45
|
return [{ count: 1 }];
|
|
45
46
|
},
|
|
46
47
|
{
|
|
47
|
-
unsafe: async (
|
|
48
|
+
unsafe: async (_query: string) => []
|
|
48
49
|
}
|
|
49
50
|
);
|
|
50
51
|
|
|
51
|
-
const result = await tableExists(mockSql,
|
|
52
|
+
const result = await tableExists(mockSql, "user", "test_db");
|
|
52
53
|
expect(result).toBe(true);
|
|
53
54
|
});
|
|
54
55
|
|
|
55
|
-
test(
|
|
56
|
+
test("表不存在时返回 false", async () => {
|
|
56
57
|
const mockSql = Object.assign(
|
|
57
|
-
async function (
|
|
58
|
+
async function (_strings: TemplateStringsArray, ..._values: any[]) {
|
|
58
59
|
return [{ count: 0 }];
|
|
59
60
|
},
|
|
60
61
|
{
|
|
61
|
-
unsafe: async (
|
|
62
|
+
unsafe: async (_query: string) => []
|
|
62
63
|
}
|
|
63
64
|
);
|
|
64
65
|
|
|
65
|
-
const result = await tableExists(mockSql,
|
|
66
|
+
const result = await tableExists(mockSql, "nonexistent", "test_db");
|
|
66
67
|
expect(result).toBe(false);
|
|
67
68
|
});
|
|
68
69
|
});
|
|
69
70
|
|
|
70
|
-
describe(
|
|
71
|
-
test(
|
|
71
|
+
describe("getTableColumns", () => {
|
|
72
|
+
test("返回正确的列信息结构", async () => {
|
|
72
73
|
const mockSql = Object.assign(
|
|
73
|
-
async function (
|
|
74
|
+
async function (_strings: TemplateStringsArray, ..._values: any[]) {
|
|
74
75
|
// 模拟 MySQL information_schema 返回
|
|
75
76
|
return [
|
|
76
77
|
{
|
|
77
|
-
COLUMN_NAME:
|
|
78
|
-
DATA_TYPE:
|
|
78
|
+
COLUMN_NAME: "id",
|
|
79
|
+
DATA_TYPE: "bigint",
|
|
79
80
|
CHARACTER_MAXIMUM_LENGTH: null,
|
|
80
|
-
IS_NULLABLE:
|
|
81
|
+
IS_NULLABLE: "NO",
|
|
81
82
|
COLUMN_DEFAULT: null,
|
|
82
|
-
COLUMN_COMMENT:
|
|
83
|
-
COLUMN_TYPE:
|
|
83
|
+
COLUMN_COMMENT: "主键ID",
|
|
84
|
+
COLUMN_TYPE: "bigint unsigned"
|
|
84
85
|
},
|
|
85
86
|
{
|
|
86
|
-
COLUMN_NAME:
|
|
87
|
-
DATA_TYPE:
|
|
87
|
+
COLUMN_NAME: "user_name",
|
|
88
|
+
DATA_TYPE: "varchar",
|
|
88
89
|
CHARACTER_MAXIMUM_LENGTH: 50,
|
|
89
|
-
IS_NULLABLE:
|
|
90
|
-
COLUMN_DEFAULT:
|
|
91
|
-
COLUMN_COMMENT:
|
|
92
|
-
COLUMN_TYPE:
|
|
90
|
+
IS_NULLABLE: "NO",
|
|
91
|
+
COLUMN_DEFAULT: "",
|
|
92
|
+
COLUMN_COMMENT: "用户名",
|
|
93
|
+
COLUMN_TYPE: "varchar(50)"
|
|
93
94
|
},
|
|
94
95
|
{
|
|
95
|
-
COLUMN_NAME:
|
|
96
|
-
DATA_TYPE:
|
|
96
|
+
COLUMN_NAME: "age",
|
|
97
|
+
DATA_TYPE: "bigint",
|
|
97
98
|
CHARACTER_MAXIMUM_LENGTH: null,
|
|
98
|
-
IS_NULLABLE:
|
|
99
|
-
COLUMN_DEFAULT:
|
|
100
|
-
COLUMN_COMMENT:
|
|
101
|
-
COLUMN_TYPE:
|
|
99
|
+
IS_NULLABLE: "YES",
|
|
100
|
+
COLUMN_DEFAULT: "0",
|
|
101
|
+
COLUMN_COMMENT: "年龄",
|
|
102
|
+
COLUMN_TYPE: "bigint"
|
|
102
103
|
}
|
|
103
104
|
];
|
|
104
105
|
},
|
|
105
106
|
{
|
|
106
|
-
unsafe: async (
|
|
107
|
+
unsafe: async (_query: string) => []
|
|
107
108
|
}
|
|
108
109
|
);
|
|
109
110
|
|
|
110
|
-
const columns = await getTableColumns(mockSql,
|
|
111
|
+
const columns = await getTableColumns(mockSql, "user", "test_db");
|
|
111
112
|
|
|
112
113
|
expect(columns.id).toBeDefined();
|
|
113
|
-
expect(columns.id.type).toBe(
|
|
114
|
+
expect(columns.id.type).toBe("bigint");
|
|
114
115
|
expect(columns.id.nullable).toBe(false);
|
|
115
|
-
expect(columns.id.comment).toBe(
|
|
116
|
+
expect(columns.id.comment).toBe("主键ID");
|
|
116
117
|
|
|
117
118
|
expect(columns.user_name).toBeDefined();
|
|
118
|
-
expect(columns.user_name.type).toBe(
|
|
119
|
+
expect(columns.user_name.type).toBe("varchar");
|
|
119
120
|
expect(columns.user_name.max).toBe(50);
|
|
120
121
|
expect(columns.user_name.nullable).toBe(false);
|
|
121
|
-
expect(columns.user_name.defaultValue).toBe(
|
|
122
|
+
expect(columns.user_name.defaultValue).toBe("");
|
|
122
123
|
|
|
123
124
|
expect(columns.age).toBeDefined();
|
|
124
125
|
expect(columns.age.nullable).toBe(true);
|
|
125
|
-
expect(columns.age.defaultValue).toBe(
|
|
126
|
+
expect(columns.age.defaultValue).toBe("0");
|
|
126
127
|
});
|
|
127
128
|
});
|
|
128
129
|
|
|
129
|
-
describe(
|
|
130
|
-
test(
|
|
130
|
+
describe("getTableIndexes", () => {
|
|
131
|
+
test("返回正确的索引信息结构", async () => {
|
|
131
132
|
const mockSql = Object.assign(
|
|
132
|
-
async function (
|
|
133
|
+
async function (_strings: TemplateStringsArray, ..._values: any[]) {
|
|
133
134
|
// 模拟 MySQL information_schema.STATISTICS 返回
|
|
134
135
|
// 注意:PRIMARY 索引被排除
|
|
135
136
|
return [
|
|
136
|
-
{ INDEX_NAME:
|
|
137
|
-
{ INDEX_NAME:
|
|
137
|
+
{ INDEX_NAME: "idx_created_at", COLUMN_NAME: "created_at" },
|
|
138
|
+
{ INDEX_NAME: "idx_user_name", COLUMN_NAME: "user_name" }
|
|
138
139
|
];
|
|
139
140
|
},
|
|
140
141
|
{
|
|
141
|
-
unsafe: async (
|
|
142
|
+
unsafe: async (_query: string) => []
|
|
142
143
|
}
|
|
143
144
|
);
|
|
144
145
|
|
|
145
|
-
const indexes = await getTableIndexes(mockSql,
|
|
146
|
+
const indexes = await getTableIndexes(mockSql, "user", "test_db");
|
|
146
147
|
|
|
147
148
|
// PRIMARY 索引被排除,不应存在
|
|
148
149
|
expect(indexes.PRIMARY).toBeUndefined();
|
|
149
150
|
|
|
150
151
|
expect(indexes.idx_created_at).toBeDefined();
|
|
151
|
-
expect(indexes.idx_created_at).toContain(
|
|
152
|
+
expect(indexes.idx_created_at).toContain("created_at");
|
|
152
153
|
|
|
153
154
|
expect(indexes.idx_user_name).toBeDefined();
|
|
154
|
-
expect(indexes.idx_user_name).toContain(
|
|
155
|
+
expect(indexes.idx_user_name).toContain("user_name");
|
|
155
156
|
});
|
|
156
157
|
|
|
157
|
-
test(
|
|
158
|
+
test("复合索引包含多个列", async () => {
|
|
158
159
|
const mockSql = Object.assign(
|
|
159
|
-
async function (
|
|
160
|
+
async function (_strings: TemplateStringsArray, ..._values: any[]) {
|
|
160
161
|
// 模拟复合索引,同一索引名包含多个列
|
|
161
162
|
return [
|
|
162
|
-
{ INDEX_NAME:
|
|
163
|
-
{ INDEX_NAME:
|
|
163
|
+
{ INDEX_NAME: "idx_composite", COLUMN_NAME: "user_id" },
|
|
164
|
+
{ INDEX_NAME: "idx_composite", COLUMN_NAME: "created_at" }
|
|
164
165
|
];
|
|
165
166
|
},
|
|
166
167
|
{
|
|
167
|
-
unsafe: async (
|
|
168
|
+
unsafe: async (_query: string) => []
|
|
168
169
|
}
|
|
169
170
|
);
|
|
170
171
|
|
|
171
|
-
const indexes = await getTableIndexes(mockSql,
|
|
172
|
+
const indexes = await getTableIndexes(mockSql, "user", "test_db");
|
|
172
173
|
|
|
173
174
|
expect(indexes.idx_composite).toBeDefined();
|
|
174
175
|
expect(indexes.idx_composite.length).toBe(2);
|
|
175
|
-
expect(indexes.idx_composite).toContain(
|
|
176
|
-
expect(indexes.idx_composite).toContain(
|
|
176
|
+
expect(indexes.idx_composite).toContain("user_id");
|
|
177
|
+
expect(indexes.idx_composite).toContain("created_at");
|
|
177
178
|
});
|
|
178
179
|
});
|
|
@@ -8,11 +8,12 @@
|
|
|
8
8
|
* - generateDefaultSql
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { describe, test, expect, beforeAll
|
|
12
|
-
|
|
11
|
+
import { describe, test, expect, beforeAll } from "bun:test";
|
|
12
|
+
|
|
13
|
+
import { setDbType } from "../sync/syncDb/constants.js";
|
|
13
14
|
|
|
14
15
|
// 设置数据库类型为 MySQL
|
|
15
|
-
setDbType(
|
|
16
|
+
setDbType("mysql");
|
|
16
17
|
|
|
17
18
|
// 动态导入以确保环境变量生效
|
|
18
19
|
let isStringOrArrayType: any;
|
|
@@ -21,118 +22,118 @@ let resolveDefaultValue: any;
|
|
|
21
22
|
let generateDefaultSql: any;
|
|
22
23
|
|
|
23
24
|
beforeAll(async () => {
|
|
24
|
-
const types = await import(
|
|
25
|
+
const types = await import("../sync/syncDb/types.js");
|
|
25
26
|
isStringOrArrayType = types.isStringOrArrayType;
|
|
26
27
|
getSqlType = types.getSqlType;
|
|
27
28
|
resolveDefaultValue = types.resolveDefaultValue;
|
|
28
29
|
generateDefaultSql = types.generateDefaultSql;
|
|
29
30
|
});
|
|
30
31
|
|
|
31
|
-
describe(
|
|
32
|
-
test(
|
|
33
|
-
expect(isStringOrArrayType(
|
|
32
|
+
describe("isStringOrArrayType", () => {
|
|
33
|
+
test("string 类型返回 true", () => {
|
|
34
|
+
expect(isStringOrArrayType("string")).toBe(true);
|
|
34
35
|
});
|
|
35
36
|
|
|
36
|
-
test(
|
|
37
|
-
expect(isStringOrArrayType(
|
|
37
|
+
test("array_string 类型返回 true", () => {
|
|
38
|
+
expect(isStringOrArrayType("array_string")).toBe(true);
|
|
38
39
|
});
|
|
39
40
|
|
|
40
|
-
test(
|
|
41
|
-
expect(isStringOrArrayType(
|
|
41
|
+
test("number 类型返回 false", () => {
|
|
42
|
+
expect(isStringOrArrayType("number")).toBe(false);
|
|
42
43
|
});
|
|
43
44
|
|
|
44
|
-
test(
|
|
45
|
-
expect(isStringOrArrayType(
|
|
45
|
+
test("text 类型返回 false", () => {
|
|
46
|
+
expect(isStringOrArrayType("text")).toBe(false);
|
|
46
47
|
});
|
|
47
48
|
|
|
48
|
-
test(
|
|
49
|
-
expect(isStringOrArrayType(
|
|
49
|
+
test("array_text 类型返回 false", () => {
|
|
50
|
+
expect(isStringOrArrayType("array_text")).toBe(false);
|
|
50
51
|
});
|
|
51
52
|
});
|
|
52
53
|
|
|
53
|
-
describe(
|
|
54
|
-
test(
|
|
55
|
-
expect(resolveDefaultValue(null,
|
|
54
|
+
describe("resolveDefaultValue", () => {
|
|
55
|
+
test("null 值 + string 类型 => 空字符串", () => {
|
|
56
|
+
expect(resolveDefaultValue(null, "string")).toBe("");
|
|
56
57
|
});
|
|
57
58
|
|
|
58
|
-
test(
|
|
59
|
-
expect(resolveDefaultValue(null,
|
|
59
|
+
test("null 值 + number 类型 => 0", () => {
|
|
60
|
+
expect(resolveDefaultValue(null, "number")).toBe(0);
|
|
60
61
|
});
|
|
61
62
|
|
|
62
63
|
test('"null" 字符串 + number 类型 => 0', () => {
|
|
63
|
-
expect(resolveDefaultValue(
|
|
64
|
+
expect(resolveDefaultValue("null", "number")).toBe(0);
|
|
64
65
|
});
|
|
65
66
|
|
|
66
67
|
test('null 值 + array_string 类型 => "[]"', () => {
|
|
67
|
-
expect(resolveDefaultValue(null,
|
|
68
|
+
expect(resolveDefaultValue(null, "array_string")).toBe("[]");
|
|
68
69
|
});
|
|
69
70
|
|
|
70
71
|
test('null 值 + text 类型 => "null"', () => {
|
|
71
|
-
expect(resolveDefaultValue(null,
|
|
72
|
+
expect(resolveDefaultValue(null, "text")).toBe("null");
|
|
72
73
|
});
|
|
73
74
|
|
|
74
75
|
test('null 值 + array_text 类型 => "null"(TEXT 不支持默认值)', () => {
|
|
75
|
-
expect(resolveDefaultValue(null,
|
|
76
|
+
expect(resolveDefaultValue(null, "array_text")).toBe("null");
|
|
76
77
|
});
|
|
77
78
|
|
|
78
|
-
test(
|
|
79
|
-
expect(resolveDefaultValue(
|
|
80
|
-
expect(resolveDefaultValue(100,
|
|
81
|
-
expect(resolveDefaultValue(0,
|
|
79
|
+
test("有实际值时直接返回", () => {
|
|
80
|
+
expect(resolveDefaultValue("admin", "string")).toBe("admin");
|
|
81
|
+
expect(resolveDefaultValue(100, "number")).toBe(100);
|
|
82
|
+
expect(resolveDefaultValue(0, "number")).toBe(0);
|
|
82
83
|
});
|
|
83
84
|
});
|
|
84
85
|
|
|
85
|
-
describe(
|
|
86
|
-
test(
|
|
87
|
-
expect(generateDefaultSql(0,
|
|
88
|
-
expect(generateDefaultSql(100,
|
|
86
|
+
describe("generateDefaultSql", () => {
|
|
87
|
+
test("number 类型生成数字默认值", () => {
|
|
88
|
+
expect(generateDefaultSql(0, "number")).toBe(" DEFAULT 0");
|
|
89
|
+
expect(generateDefaultSql(100, "number")).toBe(" DEFAULT 100");
|
|
89
90
|
});
|
|
90
91
|
|
|
91
|
-
test(
|
|
92
|
-
expect(generateDefaultSql(
|
|
93
|
-
expect(generateDefaultSql(
|
|
92
|
+
test("string 类型生成带引号默认值", () => {
|
|
93
|
+
expect(generateDefaultSql("admin", "string")).toBe(" DEFAULT 'admin'");
|
|
94
|
+
expect(generateDefaultSql("", "string")).toBe(" DEFAULT ''");
|
|
94
95
|
});
|
|
95
96
|
|
|
96
|
-
test(
|
|
97
|
-
expect(generateDefaultSql(
|
|
97
|
+
test("text 类型不生成默认值", () => {
|
|
98
|
+
expect(generateDefaultSql("null", "text")).toBe("");
|
|
98
99
|
});
|
|
99
100
|
|
|
100
|
-
test(
|
|
101
|
-
expect(generateDefaultSql(
|
|
101
|
+
test("array_string 类型生成 JSON 数组默认值", () => {
|
|
102
|
+
expect(generateDefaultSql("[]", "array_string")).toBe(" DEFAULT '[]'");
|
|
102
103
|
});
|
|
103
104
|
|
|
104
|
-
test(
|
|
105
|
-
expect(generateDefaultSql(
|
|
105
|
+
test("array_text 类型不生成默认值(MySQL TEXT 不支持)", () => {
|
|
106
|
+
expect(generateDefaultSql("[]", "array_text")).toBe("");
|
|
106
107
|
});
|
|
107
108
|
|
|
108
|
-
test(
|
|
109
|
-
expect(generateDefaultSql("it's",
|
|
109
|
+
test("单引号被正确转义", () => {
|
|
110
|
+
expect(generateDefaultSql("it's", "string")).toBe(" DEFAULT 'it''s'");
|
|
110
111
|
});
|
|
111
112
|
});
|
|
112
113
|
|
|
113
|
-
describe(
|
|
114
|
-
test(
|
|
115
|
-
const result = getSqlType(
|
|
116
|
-
expect(result).toBe(
|
|
114
|
+
describe("getSqlType", () => {
|
|
115
|
+
test("string 类型带长度", () => {
|
|
116
|
+
const result = getSqlType("string", 100);
|
|
117
|
+
expect(result).toBe("VARCHAR(100)");
|
|
117
118
|
});
|
|
118
119
|
|
|
119
|
-
test(
|
|
120
|
-
const result = getSqlType(
|
|
121
|
-
expect(result).toBe(
|
|
120
|
+
test("array_string 类型带长度", () => {
|
|
121
|
+
const result = getSqlType("array_string", 500);
|
|
122
|
+
expect(result).toBe("VARCHAR(500)");
|
|
122
123
|
});
|
|
123
124
|
|
|
124
|
-
test(
|
|
125
|
-
const result = getSqlType(
|
|
126
|
-
expect(result).toBe(
|
|
125
|
+
test("number 类型无符号", () => {
|
|
126
|
+
const result = getSqlType("number", null, true);
|
|
127
|
+
expect(result).toBe("BIGINT UNSIGNED");
|
|
127
128
|
});
|
|
128
129
|
|
|
129
|
-
test(
|
|
130
|
-
const result = getSqlType(
|
|
131
|
-
expect(result).toBe(
|
|
130
|
+
test("number 类型有符号", () => {
|
|
131
|
+
const result = getSqlType("number", null, false);
|
|
132
|
+
expect(result).toBe("BIGINT");
|
|
132
133
|
});
|
|
133
134
|
|
|
134
|
-
test(
|
|
135
|
-
const result = getSqlType(
|
|
136
|
-
expect(result).toBe(
|
|
135
|
+
test("text 类型", () => {
|
|
136
|
+
const result = getSqlType("text", null);
|
|
137
|
+
expect(result).toBe("MEDIUMTEXT");
|
|
137
138
|
});
|
|
138
139
|
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
|
|
2
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { __test__ } from "../sync/syncMenu.js";
|
|
6
|
+
|
|
7
|
+
const testRootDir = join(process.cwd(), "temp", "test-sync-menu-views");
|
|
8
|
+
const viewsDir = join(testRootDir, "views");
|
|
9
|
+
|
|
10
|
+
function writeVueIndex(dir: string, title: string, order?: number): void {
|
|
11
|
+
if (!existsSync(dir)) {
|
|
12
|
+
mkdirSync(dir, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const metaOrder = typeof order === "number" ? `, order: ${order}` : "";
|
|
16
|
+
|
|
17
|
+
const content = `<script setup>\ndefinePage({ meta: { title: "${title}"${metaOrder} } });\n</script>\n\n<template>\n <div />\n</template>\n`;
|
|
18
|
+
writeFileSync(join(dir, "index.vue"), content, { encoding: "utf8" });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
beforeAll(() => {
|
|
22
|
+
if (existsSync(testRootDir)) {
|
|
23
|
+
rmSync(testRootDir, { recursive: true, force: true });
|
|
24
|
+
}
|
|
25
|
+
mkdirSync(viewsDir, { recursive: true });
|
|
26
|
+
|
|
27
|
+
// 根级 index:应映射为 prefix 本身(不带尾随 /)
|
|
28
|
+
writeVueIndex(join(viewsDir, "index"), "Root", 2);
|
|
29
|
+
|
|
30
|
+
// 带数字后缀目录:应清理后缀
|
|
31
|
+
writeVueIndex(join(viewsDir, "user_1"), "User", 1);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterAll(() => {
|
|
35
|
+
if (existsSync(testRootDir)) {
|
|
36
|
+
rmSync(testRootDir, { recursive: true, force: true });
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("syncMenu - scanViewsDir paths", () => {
|
|
41
|
+
test("根级 index 不应生成尾随斜杠", async () => {
|
|
42
|
+
const prefix = "/addon/addonAdmin";
|
|
43
|
+
const menus = await __test__.scanViewsDir(viewsDir, prefix);
|
|
44
|
+
|
|
45
|
+
const root = menus.find((m) => m.path === prefix);
|
|
46
|
+
expect(root).toBeDefined();
|
|
47
|
+
expect(root?.path).toBe(prefix);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("目录名 _数字 后缀应被清理", async () => {
|
|
51
|
+
const prefix = "/addon/addonAdmin";
|
|
52
|
+
const menus = await __test__.scanViewsDir(viewsDir, prefix);
|
|
53
|
+
|
|
54
|
+
const user = menus.find((m) => m.path === "/addon/addonAdmin/user");
|
|
55
|
+
expect(user).toBeDefined();
|
|
56
|
+
expect(user?.name).toBe("User");
|
|
57
|
+
expect(user?.sort).toBe(1);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("syncMenu - normalizeMenuPath", () => {
|
|
62
|
+
test("去掉尾随 / 且折叠多 /", () => {
|
|
63
|
+
expect(__test__.normalizeMenuPath("/addon/a/")).toBe("/addon/a");
|
|
64
|
+
expect(__test__.normalizeMenuPath("//addon//a//b/")).toBe("/addon/a/b");
|
|
65
|
+
expect(__test__.normalizeMenuPath("addon/a")).toBe("/addon/a");
|
|
66
|
+
expect(__test__.normalizeMenuPath("/")).toBe("/");
|
|
67
|
+
});
|
|
68
|
+
});
|
package/tests/util.test.ts
CHANGED
|
@@ -1,99 +1,100 @@
|
|
|
1
|
-
|
|
2
|
-
import { keysToCamel } from 'befly-shared/keysToCamel';
|
|
3
|
-
import { keysToSnake } from 'befly-shared/keysToSnake';
|
|
4
|
-
import { arrayKeysToCamel } from 'befly-shared/arrayKeysToCamel';
|
|
5
|
-
import { calcPerfTime } from 'befly-shared/calcPerfTime';
|
|
6
|
-
import { fieldClear } from 'befly-shared/fieldClear';
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
7
2
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
3
|
+
import { arrayKeysToCamel } from "../utils/arrayKeysToCamel.js";
|
|
4
|
+
import { calcPerfTime } from "../utils/calcPerfTime.js";
|
|
5
|
+
import { fieldClear } from "../utils/fieldClear.js";
|
|
6
|
+
import { keysToCamel } from "../utils/keysToCamel.js";
|
|
7
|
+
import { keysToSnake } from "../utils/keysToSnake.js";
|
|
8
|
+
|
|
9
|
+
describe("Util - keysToCamel", () => {
|
|
10
|
+
test("转换对象键名为驼峰", () => {
|
|
11
|
+
const result = keysToCamel({ user_name: "John", user_id: 123 });
|
|
12
|
+
expect(result.userName).toBe("John");
|
|
12
13
|
expect(result.userId).toBe(123);
|
|
13
14
|
});
|
|
14
15
|
|
|
15
|
-
test(
|
|
16
|
-
const result = keysToCamel({ userName:
|
|
17
|
-
expect(result.userName).toBe(
|
|
16
|
+
test("保持已有驼峰格式", () => {
|
|
17
|
+
const result = keysToCamel({ userName: "John", userId: 123 });
|
|
18
|
+
expect(result.userName).toBe("John");
|
|
18
19
|
expect(result.userId).toBe(123);
|
|
19
20
|
});
|
|
20
21
|
|
|
21
|
-
test(
|
|
22
|
+
test("处理空对象", () => {
|
|
22
23
|
const result = keysToCamel({});
|
|
23
24
|
expect(Object.keys(result).length).toBe(0);
|
|
24
25
|
});
|
|
25
26
|
|
|
26
|
-
test(
|
|
27
|
-
const result = keysToCamel({ user_info: { first_name:
|
|
27
|
+
test("处理嵌套对象", () => {
|
|
28
|
+
const result = keysToCamel({ user_info: { first_name: "John" } });
|
|
28
29
|
expect(result.userInfo).toBeDefined();
|
|
29
30
|
});
|
|
30
31
|
});
|
|
31
32
|
|
|
32
|
-
describe(
|
|
33
|
-
test(
|
|
34
|
-
const result = keysToSnake({ userName:
|
|
35
|
-
expect(result.user_name).toBe(
|
|
33
|
+
describe("Util - keysToSnake", () => {
|
|
34
|
+
test("转换对象键名为下划线", () => {
|
|
35
|
+
const result = keysToSnake({ userName: "John", userId: 123 });
|
|
36
|
+
expect(result.user_name).toBe("John");
|
|
36
37
|
expect(result.user_id).toBe(123);
|
|
37
38
|
});
|
|
38
39
|
|
|
39
|
-
test(
|
|
40
|
-
const result = keysToSnake({ user_name:
|
|
41
|
-
expect(result.user_name).toBe(
|
|
40
|
+
test("保持已有下划线格式", () => {
|
|
41
|
+
const result = keysToSnake({ user_name: "John", user_id: 123 });
|
|
42
|
+
expect(result.user_name).toBe("John");
|
|
42
43
|
expect(result.user_id).toBe(123);
|
|
43
44
|
});
|
|
44
45
|
|
|
45
|
-
test(
|
|
46
|
+
test("处理空对象", () => {
|
|
46
47
|
const result = keysToSnake({});
|
|
47
48
|
expect(Object.keys(result).length).toBe(0);
|
|
48
49
|
});
|
|
49
50
|
});
|
|
50
51
|
|
|
51
|
-
describe(
|
|
52
|
-
test(
|
|
52
|
+
describe("Util - arrayKeysToCamel", () => {
|
|
53
|
+
test("转换数组中对象键名为驼峰", () => {
|
|
53
54
|
const result = arrayKeysToCamel([
|
|
54
|
-
{ user_name:
|
|
55
|
-
{ user_name:
|
|
55
|
+
{ user_name: "John", user_id: 1 },
|
|
56
|
+
{ user_name: "Jane", user_id: 2 }
|
|
56
57
|
]);
|
|
57
|
-
expect(result[0].userName).toBe(
|
|
58
|
+
expect(result[0].userName).toBe("John");
|
|
58
59
|
expect(result[0].userId).toBe(1);
|
|
59
|
-
expect(result[1].userName).toBe(
|
|
60
|
+
expect(result[1].userName).toBe("Jane");
|
|
60
61
|
expect(result[1].userId).toBe(2);
|
|
61
62
|
});
|
|
62
63
|
|
|
63
|
-
test(
|
|
64
|
+
test("处理空数组", () => {
|
|
64
65
|
const result = arrayKeysToCamel([]);
|
|
65
66
|
expect(result.length).toBe(0);
|
|
66
67
|
});
|
|
67
68
|
});
|
|
68
69
|
|
|
69
|
-
describe(
|
|
70
|
-
test(
|
|
71
|
-
const result = fieldClear({ a: 1, b: null, c: undefined, d:
|
|
70
|
+
describe("Util - fieldClear", () => {
|
|
71
|
+
test("移除 null 和 undefined", () => {
|
|
72
|
+
const result = fieldClear({ a: 1, b: null, c: undefined, d: "test" }, { excludeValues: [null, undefined] });
|
|
72
73
|
expect(result.a).toBe(1);
|
|
73
74
|
expect(result.b).toBeUndefined();
|
|
74
75
|
expect(result.c).toBeUndefined();
|
|
75
|
-
expect(result.d).toBe(
|
|
76
|
+
expect(result.d).toBe("test");
|
|
76
77
|
});
|
|
77
78
|
|
|
78
|
-
test(
|
|
79
|
+
test("保留指定值", () => {
|
|
79
80
|
const result = fieldClear({ a: 1, b: null, c: 0 }, { excludeValues: [null, undefined], keepMap: { c: 0 } });
|
|
80
81
|
expect(result.a).toBe(1);
|
|
81
82
|
expect(result.b).toBeUndefined();
|
|
82
83
|
expect(result.c).toBe(0);
|
|
83
84
|
});
|
|
84
85
|
|
|
85
|
-
test(
|
|
86
|
+
test("处理空对象", () => {
|
|
86
87
|
const result = fieldClear({});
|
|
87
88
|
expect(Object.keys(result).length).toBe(0);
|
|
88
89
|
});
|
|
89
90
|
});
|
|
90
91
|
|
|
91
|
-
describe(
|
|
92
|
-
test(
|
|
92
|
+
describe("Util - calcPerfTime", () => {
|
|
93
|
+
test("计算性能时间", () => {
|
|
93
94
|
const start = Bun.nanoseconds();
|
|
94
95
|
const result = calcPerfTime(start);
|
|
95
|
-
expect(result).toContain(
|
|
96
|
-
expect(typeof result).toBe(
|
|
96
|
+
expect(result).toContain("毫秒");
|
|
97
|
+
expect(typeof result).toBe("string");
|
|
97
98
|
expect(result).toMatch(/\d+(\.\d+)?\s*毫秒/);
|
|
98
99
|
});
|
|
99
100
|
});
|