befly 3.8.25 → 3.8.27
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/config.ts +8 -9
- package/hooks/{rateLimit.ts → _rateLimit.ts} +7 -13
- package/hooks/auth.ts +3 -11
- package/hooks/cors.ts +1 -4
- package/hooks/parser.ts +6 -8
- package/hooks/permission.ts +9 -12
- package/hooks/validator.ts +6 -9
- package/lib/cacheHelper.ts +0 -4
- package/lib/{database.ts → connect.ts} +65 -18
- package/lib/logger.ts +1 -17
- package/lib/redisHelper.ts +6 -5
- package/loader/loadApis.ts +3 -3
- package/loader/loadHooks.ts +15 -41
- package/loader/loadPlugins.ts +10 -16
- package/main.ts +25 -28
- package/package.json +3 -3
- package/plugins/cache.ts +2 -2
- package/plugins/cipher.ts +15 -0
- package/plugins/config.ts +16 -0
- package/plugins/db.ts +7 -17
- package/plugins/jwt.ts +15 -0
- package/plugins/logger.ts +1 -1
- package/plugins/redis.ts +4 -4
- package/plugins/tool.ts +50 -0
- package/router/api.ts +56 -42
- package/router/static.ts +12 -12
- package/sync/syncAll.ts +2 -20
- package/sync/syncApi.ts +7 -7
- package/sync/syncDb/apply.ts +1 -4
- package/sync/syncDb/constants.ts +3 -0
- package/sync/syncDb/ddl.ts +2 -1
- package/sync/syncDb/helpers.ts +5 -117
- package/sync/syncDb/sqlite.ts +1 -3
- package/sync/syncDb/table.ts +8 -142
- package/sync/syncDb/tableCreate.ts +25 -9
- package/sync/syncDb/types.ts +125 -0
- package/sync/syncDb/version.ts +0 -3
- package/sync/syncDb.ts +146 -6
- package/sync/syncDev.ts +19 -15
- package/sync/syncMenu.ts +87 -75
- package/tests/redisHelper.test.ts +15 -16
- package/tests/sync-connection.test.ts +189 -0
- package/tests/syncDb-apply.test.ts +287 -0
- package/tests/syncDb-constants.test.ts +150 -0
- package/tests/syncDb-ddl.test.ts +205 -0
- package/tests/syncDb-helpers.test.ts +112 -0
- package/tests/syncDb-schema.test.ts +178 -0
- package/tests/syncDb-types.test.ts +129 -0
- package/tsconfig.json +2 -2
- package/types/api.d.ts +1 -1
- package/types/befly.d.ts +23 -21
- package/types/common.d.ts +0 -29
- package/types/context.d.ts +8 -6
- package/types/hook.d.ts +3 -4
- package/types/plugin.d.ts +3 -0
- package/hooks/errorHandler.ts +0 -23
- package/hooks/requestId.ts +0 -24
- package/hooks/requestLogger.ts +0 -25
- package/hooks/responseFormatter.ts +0 -64
- package/router/root.ts +0 -56
- package/sync/syncDb/index.ts +0 -164
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* syncDb DDL 构建模块测试
|
|
3
|
+
*
|
|
4
|
+
* 测试 ddl.ts 中的函数:
|
|
5
|
+
* - buildIndexSQL
|
|
6
|
+
* - buildSystemColumnDefs
|
|
7
|
+
* - buildBusinessColumnDefs
|
|
8
|
+
* - generateDDLClause
|
|
9
|
+
* - isPgCompatibleTypeChange
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, test, expect, beforeAll } from 'bun:test';
|
|
13
|
+
|
|
14
|
+
// 设置环境变量模拟 MySQL 环境
|
|
15
|
+
process.env.DB_TYPE = 'mysql';
|
|
16
|
+
|
|
17
|
+
let buildIndexSQL: any;
|
|
18
|
+
let buildSystemColumnDefs: any;
|
|
19
|
+
let buildBusinessColumnDefs: any;
|
|
20
|
+
let generateDDLClause: any;
|
|
21
|
+
let isPgCompatibleTypeChange: any;
|
|
22
|
+
|
|
23
|
+
beforeAll(async () => {
|
|
24
|
+
const ddl = await import('../sync/syncDb/ddl.js');
|
|
25
|
+
buildIndexSQL = ddl.buildIndexSQL;
|
|
26
|
+
buildSystemColumnDefs = ddl.buildSystemColumnDefs;
|
|
27
|
+
buildBusinessColumnDefs = ddl.buildBusinessColumnDefs;
|
|
28
|
+
generateDDLClause = ddl.generateDDLClause;
|
|
29
|
+
isPgCompatibleTypeChange = ddl.isPgCompatibleTypeChange;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('buildIndexSQL (MySQL)', () => {
|
|
33
|
+
test('创建索引 SQL', () => {
|
|
34
|
+
const sql = buildIndexSQL('user', 'idx_created_at', 'created_at', 'create');
|
|
35
|
+
expect(sql).toContain('ALTER TABLE `user`');
|
|
36
|
+
expect(sql).toContain('ADD INDEX `idx_created_at`');
|
|
37
|
+
expect(sql).toContain('(`created_at`)');
|
|
38
|
+
expect(sql).toContain('ALGORITHM=INPLACE');
|
|
39
|
+
expect(sql).toContain('LOCK=NONE');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('删除索引 SQL', () => {
|
|
43
|
+
const sql = buildIndexSQL('user', 'idx_created_at', 'created_at', 'drop');
|
|
44
|
+
expect(sql).toContain('ALTER TABLE `user`');
|
|
45
|
+
expect(sql).toContain('DROP INDEX `idx_created_at`');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('buildSystemColumnDefs (MySQL)', () => {
|
|
50
|
+
test('返回 5 个系统字段定义', () => {
|
|
51
|
+
const defs = buildSystemColumnDefs();
|
|
52
|
+
expect(defs.length).toBe(5);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('包含 id 主键', () => {
|
|
56
|
+
const defs = buildSystemColumnDefs();
|
|
57
|
+
const idDef = defs.find((d: string) => d.includes('`id`'));
|
|
58
|
+
expect(idDef).toContain('PRIMARY KEY');
|
|
59
|
+
expect(idDef).toContain('AUTO_INCREMENT');
|
|
60
|
+
expect(idDef).toContain('BIGINT UNSIGNED');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('包含 created_at 字段', () => {
|
|
64
|
+
const defs = buildSystemColumnDefs();
|
|
65
|
+
const def = defs.find((d: string) => d.includes('`created_at`'));
|
|
66
|
+
expect(def).toContain('BIGINT UNSIGNED');
|
|
67
|
+
expect(def).toContain('NOT NULL');
|
|
68
|
+
expect(def).toContain('DEFAULT 0');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('包含 state 字段', () => {
|
|
72
|
+
const defs = buildSystemColumnDefs();
|
|
73
|
+
const def = defs.find((d: string) => d.includes('`state`'));
|
|
74
|
+
expect(def).toContain('BIGINT UNSIGNED');
|
|
75
|
+
expect(def).toContain('NOT NULL');
|
|
76
|
+
expect(def).toContain('DEFAULT 0');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('buildBusinessColumnDefs (MySQL)', () => {
|
|
81
|
+
test('生成 string 类型字段', () => {
|
|
82
|
+
const fields = {
|
|
83
|
+
userName: {
|
|
84
|
+
name: '用户名',
|
|
85
|
+
type: 'string',
|
|
86
|
+
max: 50,
|
|
87
|
+
default: null,
|
|
88
|
+
unique: false,
|
|
89
|
+
nullable: false,
|
|
90
|
+
unsigned: true
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
const defs = buildBusinessColumnDefs(fields);
|
|
94
|
+
expect(defs.length).toBe(1);
|
|
95
|
+
expect(defs[0]).toContain('`user_name`');
|
|
96
|
+
expect(defs[0]).toContain('VARCHAR(50)');
|
|
97
|
+
expect(defs[0]).toContain('NOT NULL');
|
|
98
|
+
expect(defs[0]).toContain("DEFAULT ''");
|
|
99
|
+
expect(defs[0]).toContain('COMMENT "用户名"');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('生成 number 类型字段', () => {
|
|
103
|
+
const fields = {
|
|
104
|
+
age: {
|
|
105
|
+
name: '年龄',
|
|
106
|
+
type: 'number',
|
|
107
|
+
max: null,
|
|
108
|
+
default: 0,
|
|
109
|
+
unique: false,
|
|
110
|
+
nullable: false,
|
|
111
|
+
unsigned: true
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
const defs = buildBusinessColumnDefs(fields);
|
|
115
|
+
expect(defs[0]).toContain('`age`');
|
|
116
|
+
expect(defs[0]).toContain('BIGINT UNSIGNED');
|
|
117
|
+
expect(defs[0]).toContain('DEFAULT 0');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('生成 unique 字段', () => {
|
|
121
|
+
const fields = {
|
|
122
|
+
email: {
|
|
123
|
+
name: '邮箱',
|
|
124
|
+
type: 'string',
|
|
125
|
+
max: 100,
|
|
126
|
+
default: null,
|
|
127
|
+
unique: true,
|
|
128
|
+
nullable: false,
|
|
129
|
+
unsigned: true
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
const defs = buildBusinessColumnDefs(fields);
|
|
133
|
+
expect(defs[0]).toContain('UNIQUE');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('生成 nullable 字段', () => {
|
|
137
|
+
const fields = {
|
|
138
|
+
remark: {
|
|
139
|
+
name: '备注',
|
|
140
|
+
type: 'string',
|
|
141
|
+
max: 200,
|
|
142
|
+
default: null,
|
|
143
|
+
unique: false,
|
|
144
|
+
nullable: true,
|
|
145
|
+
unsigned: true
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
const defs = buildBusinessColumnDefs(fields);
|
|
149
|
+
expect(defs[0]).toContain('NULL');
|
|
150
|
+
expect(defs[0]).not.toContain('NOT NULL');
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('generateDDLClause (MySQL)', () => {
|
|
155
|
+
test('生成 ADD COLUMN 子句', () => {
|
|
156
|
+
const fieldDef = {
|
|
157
|
+
name: '用户名',
|
|
158
|
+
type: 'string',
|
|
159
|
+
max: 50,
|
|
160
|
+
default: null,
|
|
161
|
+
unique: false,
|
|
162
|
+
nullable: false,
|
|
163
|
+
unsigned: true
|
|
164
|
+
};
|
|
165
|
+
const clause = generateDDLClause('userName', fieldDef, true);
|
|
166
|
+
expect(clause).toContain('ADD COLUMN');
|
|
167
|
+
expect(clause).toContain('`user_name`');
|
|
168
|
+
expect(clause).toContain('VARCHAR(50)');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('生成 MODIFY COLUMN 子句', () => {
|
|
172
|
+
const fieldDef = {
|
|
173
|
+
name: '用户名',
|
|
174
|
+
type: 'string',
|
|
175
|
+
max: 100,
|
|
176
|
+
default: null,
|
|
177
|
+
unique: false,
|
|
178
|
+
nullable: false,
|
|
179
|
+
unsigned: true
|
|
180
|
+
};
|
|
181
|
+
const clause = generateDDLClause('userName', fieldDef, false);
|
|
182
|
+
expect(clause).toContain('MODIFY COLUMN');
|
|
183
|
+
expect(clause).toContain('`user_name`');
|
|
184
|
+
expect(clause).toContain('VARCHAR(100)');
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe('isPgCompatibleTypeChange', () => {
|
|
189
|
+
test('varchar -> text 是兼容变更', () => {
|
|
190
|
+
expect(isPgCompatibleTypeChange('character varying', 'text')).toBe(true);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('text -> varchar 不是兼容变更', () => {
|
|
194
|
+
expect(isPgCompatibleTypeChange('text', 'character varying')).toBe(false);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('相同类型不是变更', () => {
|
|
198
|
+
expect(isPgCompatibleTypeChange('text', 'text')).toBe(false);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test('空值处理', () => {
|
|
202
|
+
expect(isPgCompatibleTypeChange(null, 'text')).toBe(false);
|
|
203
|
+
expect(isPgCompatibleTypeChange('text', null)).toBe(false);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* syncDb 辅助工具模块测试
|
|
3
|
+
*
|
|
4
|
+
* 测试 helpers.ts 中的函数:
|
|
5
|
+
* - quoteIdentifier
|
|
6
|
+
* - escapeComment
|
|
7
|
+
* - applyFieldDefaults
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, test, expect, beforeAll } from 'bun:test';
|
|
11
|
+
|
|
12
|
+
// 设置环境变量模拟 MySQL 环境
|
|
13
|
+
process.env.DB_TYPE = 'mysql';
|
|
14
|
+
|
|
15
|
+
let quoteIdentifier: any;
|
|
16
|
+
let escapeComment: any;
|
|
17
|
+
let applyFieldDefaults: any;
|
|
18
|
+
|
|
19
|
+
beforeAll(async () => {
|
|
20
|
+
const helpers = await import('../sync/syncDb/helpers.js');
|
|
21
|
+
quoteIdentifier = helpers.quoteIdentifier;
|
|
22
|
+
escapeComment = helpers.escapeComment;
|
|
23
|
+
applyFieldDefaults = helpers.applyFieldDefaults;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('quoteIdentifier (MySQL)', () => {
|
|
27
|
+
test('使用反引号包裹标识符', () => {
|
|
28
|
+
expect(quoteIdentifier('user_table')).toBe('`user_table`');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('处理普通表名', () => {
|
|
32
|
+
expect(quoteIdentifier('admin')).toBe('`admin`');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('处理带下划线的表名', () => {
|
|
36
|
+
expect(quoteIdentifier('addon_admin_menu')).toBe('`addon_admin_menu`');
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('escapeComment', () => {
|
|
41
|
+
test('普通注释不变', () => {
|
|
42
|
+
expect(escapeComment('用户名称')).toBe('用户名称');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('双引号被转义', () => {
|
|
46
|
+
expect(escapeComment('用户"昵称"')).toBe('用户\\"昵称\\"');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('空字符串', () => {
|
|
50
|
+
expect(escapeComment('')).toBe('');
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('applyFieldDefaults', () => {
|
|
55
|
+
test('为空字段定义应用默认值', () => {
|
|
56
|
+
const fieldDef: any = {
|
|
57
|
+
name: '用户名',
|
|
58
|
+
type: 'string'
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
applyFieldDefaults(fieldDef);
|
|
62
|
+
|
|
63
|
+
expect(fieldDef.detail).toBe('');
|
|
64
|
+
expect(fieldDef.min).toBe(0);
|
|
65
|
+
expect(fieldDef.max).toBe(100);
|
|
66
|
+
expect(fieldDef.default).toBe(null);
|
|
67
|
+
expect(fieldDef.index).toBe(false);
|
|
68
|
+
expect(fieldDef.unique).toBe(false);
|
|
69
|
+
expect(fieldDef.comment).toBe('');
|
|
70
|
+
expect(fieldDef.nullable).toBe(false);
|
|
71
|
+
expect(fieldDef.unsigned).toBe(true);
|
|
72
|
+
expect(fieldDef.regexp).toBe(null);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('保留已有值', () => {
|
|
76
|
+
const fieldDef: any = {
|
|
77
|
+
name: '用户名',
|
|
78
|
+
type: 'string',
|
|
79
|
+
max: 200,
|
|
80
|
+
index: true,
|
|
81
|
+
unique: true,
|
|
82
|
+
nullable: true
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
applyFieldDefaults(fieldDef);
|
|
86
|
+
|
|
87
|
+
expect(fieldDef.max).toBe(200);
|
|
88
|
+
expect(fieldDef.index).toBe(true);
|
|
89
|
+
expect(fieldDef.unique).toBe(true);
|
|
90
|
+
expect(fieldDef.nullable).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('处理 0 和 false 值', () => {
|
|
94
|
+
const fieldDef: any = {
|
|
95
|
+
name: '排序',
|
|
96
|
+
type: 'number',
|
|
97
|
+
min: 0,
|
|
98
|
+
max: 0,
|
|
99
|
+
default: 0,
|
|
100
|
+
index: false,
|
|
101
|
+
unsigned: false
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
applyFieldDefaults(fieldDef);
|
|
105
|
+
|
|
106
|
+
expect(fieldDef.min).toBe(0);
|
|
107
|
+
expect(fieldDef.max).toBe(0);
|
|
108
|
+
expect(fieldDef.default).toBe(0);
|
|
109
|
+
expect(fieldDef.index).toBe(false);
|
|
110
|
+
expect(fieldDef.unsigned).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* syncDb 表结构查询模块测试
|
|
3
|
+
*
|
|
4
|
+
* 测试 schema.ts 中的函数(纯逻辑测试,不需要数据库连接):
|
|
5
|
+
* - tableExists
|
|
6
|
+
* - getTableColumns
|
|
7
|
+
* - getTableIndexes
|
|
8
|
+
*
|
|
9
|
+
* 注意:这些是模拟测试,实际数据库操作需要集成测试
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, test, expect, beforeAll, mock } from 'bun:test';
|
|
13
|
+
|
|
14
|
+
// 设置环境变量模拟 MySQL 环境
|
|
15
|
+
process.env.DB_TYPE = 'mysql';
|
|
16
|
+
process.env.DB_NAME = 'test_db';
|
|
17
|
+
|
|
18
|
+
let tableExists: any;
|
|
19
|
+
let getTableColumns: any;
|
|
20
|
+
let getTableIndexes: any;
|
|
21
|
+
|
|
22
|
+
beforeAll(async () => {
|
|
23
|
+
const schema = await import('../sync/syncDb/schema.js');
|
|
24
|
+
tableExists = schema.tableExists;
|
|
25
|
+
getTableColumns = schema.getTableColumns;
|
|
26
|
+
getTableIndexes = schema.getTableIndexes;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('tableExists', () => {
|
|
30
|
+
test('sql 客户端未初始化时抛出错误', async () => {
|
|
31
|
+
try {
|
|
32
|
+
await tableExists(null, 'user');
|
|
33
|
+
expect(true).toBe(false); // 不应该到这里
|
|
34
|
+
} catch (error: any) {
|
|
35
|
+
expect(error.message).toBe('SQL 客户端未初始化');
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('传入有效 sql 客户端时正常执行', async () => {
|
|
40
|
+
// 创建模拟 SQL 客户端
|
|
41
|
+
const mockSql = Object.assign(
|
|
42
|
+
async function (strings: TemplateStringsArray, ...values: any[]) {
|
|
43
|
+
// 模拟 MySQL 查询返回
|
|
44
|
+
return [{ count: 1 }];
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
unsafe: async (query: string) => []
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const result = await tableExists(mockSql, 'user', 'test_db');
|
|
52
|
+
expect(result).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('表不存在时返回 false', async () => {
|
|
56
|
+
const mockSql = Object.assign(
|
|
57
|
+
async function (strings: TemplateStringsArray, ...values: any[]) {
|
|
58
|
+
return [{ count: 0 }];
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
unsafe: async (query: string) => []
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const result = await tableExists(mockSql, 'nonexistent', 'test_db');
|
|
66
|
+
expect(result).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('getTableColumns', () => {
|
|
71
|
+
test('返回正确的列信息结构', async () => {
|
|
72
|
+
const mockSql = Object.assign(
|
|
73
|
+
async function (strings: TemplateStringsArray, ...values: any[]) {
|
|
74
|
+
// 模拟 MySQL information_schema 返回
|
|
75
|
+
return [
|
|
76
|
+
{
|
|
77
|
+
COLUMN_NAME: 'id',
|
|
78
|
+
DATA_TYPE: 'bigint',
|
|
79
|
+
CHARACTER_MAXIMUM_LENGTH: null,
|
|
80
|
+
IS_NULLABLE: 'NO',
|
|
81
|
+
COLUMN_DEFAULT: null,
|
|
82
|
+
COLUMN_COMMENT: '主键ID',
|
|
83
|
+
COLUMN_TYPE: 'bigint unsigned'
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
COLUMN_NAME: 'user_name',
|
|
87
|
+
DATA_TYPE: 'varchar',
|
|
88
|
+
CHARACTER_MAXIMUM_LENGTH: 50,
|
|
89
|
+
IS_NULLABLE: 'NO',
|
|
90
|
+
COLUMN_DEFAULT: '',
|
|
91
|
+
COLUMN_COMMENT: '用户名',
|
|
92
|
+
COLUMN_TYPE: 'varchar(50)'
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
COLUMN_NAME: 'age',
|
|
96
|
+
DATA_TYPE: 'bigint',
|
|
97
|
+
CHARACTER_MAXIMUM_LENGTH: null,
|
|
98
|
+
IS_NULLABLE: 'YES',
|
|
99
|
+
COLUMN_DEFAULT: '0',
|
|
100
|
+
COLUMN_COMMENT: '年龄',
|
|
101
|
+
COLUMN_TYPE: 'bigint'
|
|
102
|
+
}
|
|
103
|
+
];
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
unsafe: async (query: string) => []
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const columns = await getTableColumns(mockSql, 'user', 'test_db');
|
|
111
|
+
|
|
112
|
+
expect(columns.id).toBeDefined();
|
|
113
|
+
expect(columns.id.type).toBe('bigint');
|
|
114
|
+
expect(columns.id.nullable).toBe(false);
|
|
115
|
+
expect(columns.id.comment).toBe('主键ID');
|
|
116
|
+
|
|
117
|
+
expect(columns.user_name).toBeDefined();
|
|
118
|
+
expect(columns.user_name.type).toBe('varchar');
|
|
119
|
+
expect(columns.user_name.max).toBe(50);
|
|
120
|
+
expect(columns.user_name.nullable).toBe(false);
|
|
121
|
+
expect(columns.user_name.defaultValue).toBe('');
|
|
122
|
+
|
|
123
|
+
expect(columns.age).toBeDefined();
|
|
124
|
+
expect(columns.age.nullable).toBe(true);
|
|
125
|
+
expect(columns.age.defaultValue).toBe('0');
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('getTableIndexes', () => {
|
|
130
|
+
test('返回正确的索引信息结构', async () => {
|
|
131
|
+
const mockSql = Object.assign(
|
|
132
|
+
async function (strings: TemplateStringsArray, ...values: any[]) {
|
|
133
|
+
// 模拟 MySQL information_schema.STATISTICS 返回
|
|
134
|
+
// 注意:PRIMARY 索引被排除
|
|
135
|
+
return [
|
|
136
|
+
{ INDEX_NAME: 'idx_created_at', COLUMN_NAME: 'created_at' },
|
|
137
|
+
{ INDEX_NAME: 'idx_user_name', COLUMN_NAME: 'user_name' }
|
|
138
|
+
];
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
unsafe: async (query: string) => []
|
|
142
|
+
}
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const indexes = await getTableIndexes(mockSql, 'user', 'test_db');
|
|
146
|
+
|
|
147
|
+
// PRIMARY 索引被排除,不应存在
|
|
148
|
+
expect(indexes.PRIMARY).toBeUndefined();
|
|
149
|
+
|
|
150
|
+
expect(indexes.idx_created_at).toBeDefined();
|
|
151
|
+
expect(indexes.idx_created_at).toContain('created_at');
|
|
152
|
+
|
|
153
|
+
expect(indexes.idx_user_name).toBeDefined();
|
|
154
|
+
expect(indexes.idx_user_name).toContain('user_name');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('复合索引包含多个列', async () => {
|
|
158
|
+
const mockSql = Object.assign(
|
|
159
|
+
async function (strings: TemplateStringsArray, ...values: any[]) {
|
|
160
|
+
// 模拟复合索引,同一索引名包含多个列
|
|
161
|
+
return [
|
|
162
|
+
{ INDEX_NAME: 'idx_composite', COLUMN_NAME: 'user_id' },
|
|
163
|
+
{ INDEX_NAME: 'idx_composite', COLUMN_NAME: 'created_at' }
|
|
164
|
+
];
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
unsafe: async (query: string) => []
|
|
168
|
+
}
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const indexes = await getTableIndexes(mockSql, 'user', 'test_db');
|
|
172
|
+
|
|
173
|
+
expect(indexes.idx_composite).toBeDefined();
|
|
174
|
+
expect(indexes.idx_composite.length).toBe(2);
|
|
175
|
+
expect(indexes.idx_composite).toContain('user_id');
|
|
176
|
+
expect(indexes.idx_composite).toContain('created_at');
|
|
177
|
+
});
|
|
178
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* syncDb 类型处理模块测试
|
|
3
|
+
*
|
|
4
|
+
* 测试 types.ts 中的函数:
|
|
5
|
+
* - isStringOrArrayType
|
|
6
|
+
* - getSqlType
|
|
7
|
+
* - resolveDefaultValue
|
|
8
|
+
* - generateDefaultSql
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
|
|
12
|
+
|
|
13
|
+
// 设置环境变量模拟 MySQL 环境
|
|
14
|
+
process.env.DB_TYPE = 'mysql';
|
|
15
|
+
|
|
16
|
+
// 动态导入以确保环境变量生效
|
|
17
|
+
let isStringOrArrayType: any;
|
|
18
|
+
let getSqlType: any;
|
|
19
|
+
let resolveDefaultValue: any;
|
|
20
|
+
let generateDefaultSql: any;
|
|
21
|
+
|
|
22
|
+
beforeAll(async () => {
|
|
23
|
+
const types = await import('../sync/syncDb/types.js');
|
|
24
|
+
isStringOrArrayType = types.isStringOrArrayType;
|
|
25
|
+
getSqlType = types.getSqlType;
|
|
26
|
+
resolveDefaultValue = types.resolveDefaultValue;
|
|
27
|
+
generateDefaultSql = types.generateDefaultSql;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('isStringOrArrayType', () => {
|
|
31
|
+
test('string 类型返回 true', () => {
|
|
32
|
+
expect(isStringOrArrayType('string')).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('array_string 类型返回 true', () => {
|
|
36
|
+
expect(isStringOrArrayType('array_string')).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('number 类型返回 false', () => {
|
|
40
|
+
expect(isStringOrArrayType('number')).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('text 类型返回 false', () => {
|
|
44
|
+
expect(isStringOrArrayType('text')).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('array_text 类型返回 false', () => {
|
|
48
|
+
expect(isStringOrArrayType('array_text')).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('resolveDefaultValue', () => {
|
|
53
|
+
test('null 值 + string 类型 => 空字符串', () => {
|
|
54
|
+
expect(resolveDefaultValue(null, 'string')).toBe('');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('null 值 + number 类型 => 0', () => {
|
|
58
|
+
expect(resolveDefaultValue(null, 'number')).toBe(0);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('"null" 字符串 + number 类型 => 0', () => {
|
|
62
|
+
expect(resolveDefaultValue('null', 'number')).toBe(0);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('null 值 + array 类型 => "[]"', () => {
|
|
66
|
+
expect(resolveDefaultValue(null, 'array')).toBe('[]');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('null 值 + text 类型 => "null"', () => {
|
|
70
|
+
expect(resolveDefaultValue(null, 'text')).toBe('null');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('有实际值时直接返回', () => {
|
|
74
|
+
expect(resolveDefaultValue('admin', 'string')).toBe('admin');
|
|
75
|
+
expect(resolveDefaultValue(100, 'number')).toBe(100);
|
|
76
|
+
expect(resolveDefaultValue(0, 'number')).toBe(0);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('generateDefaultSql', () => {
|
|
81
|
+
test('number 类型生成数字默认值', () => {
|
|
82
|
+
expect(generateDefaultSql(0, 'number')).toBe(' DEFAULT 0');
|
|
83
|
+
expect(generateDefaultSql(100, 'number')).toBe(' DEFAULT 100');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('string 类型生成带引号默认值', () => {
|
|
87
|
+
expect(generateDefaultSql('admin', 'string')).toBe(" DEFAULT 'admin'");
|
|
88
|
+
expect(generateDefaultSql('', 'string')).toBe(" DEFAULT ''");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('text 类型不生成默认值', () => {
|
|
92
|
+
expect(generateDefaultSql('null', 'text')).toBe('');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('array 类型生成 JSON 数组默认值', () => {
|
|
96
|
+
expect(generateDefaultSql('[]', 'array')).toBe(" DEFAULT '[]'");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('单引号被正确转义', () => {
|
|
100
|
+
expect(generateDefaultSql("it's", 'string')).toBe(" DEFAULT 'it''s'");
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('getSqlType', () => {
|
|
105
|
+
test('string 类型带长度', () => {
|
|
106
|
+
const result = getSqlType('string', 100);
|
|
107
|
+
expect(result).toBe('VARCHAR(100)');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('array_string 类型带长度', () => {
|
|
111
|
+
const result = getSqlType('array_string', 500);
|
|
112
|
+
expect(result).toBe('VARCHAR(500)');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('number 类型无符号', () => {
|
|
116
|
+
const result = getSqlType('number', null, true);
|
|
117
|
+
expect(result).toBe('BIGINT UNSIGNED');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('number 类型有符号', () => {
|
|
121
|
+
const result = getSqlType('number', null, false);
|
|
122
|
+
expect(result).toBe('BIGINT');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('text 类型', () => {
|
|
126
|
+
const result = getSqlType('text', null);
|
|
127
|
+
expect(result).toBe('MEDIUMTEXT');
|
|
128
|
+
});
|
|
129
|
+
});
|
package/tsconfig.json
CHANGED
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
"alwaysStrict": true,
|
|
18
18
|
|
|
19
19
|
// 额外检查
|
|
20
|
-
"noUnusedLocals":
|
|
21
|
-
"noUnusedParameters":
|
|
20
|
+
"noUnusedLocals": false,
|
|
21
|
+
"noUnusedParameters": false,
|
|
22
22
|
"noImplicitReturns": true,
|
|
23
23
|
"noFallthroughCasesInSwitch": true,
|
|
24
24
|
"noUncheckedIndexedAccess": false,
|
package/types/api.d.ts
CHANGED
|
@@ -35,7 +35,7 @@ export type AuthType = boolean | 'admin' | 'user' | string[];
|
|
|
35
35
|
/**
|
|
36
36
|
* API 处理器函数类型
|
|
37
37
|
*/
|
|
38
|
-
export type ApiHandler<T = any, R = any> = (befly: BeflyContext, ctx: RequestContext
|
|
38
|
+
export type ApiHandler<T = any, R = any> = (befly: BeflyContext, ctx: RequestContext) => Promise<Response | R> | Response | R;
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
41
|
* 字段规则定义
|