befly 3.0.0 → 3.0.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/checks/conflict.ts +35 -114
- package/checks/table.ts +31 -63
- package/config/env.ts +3 -3
- package/config/fields.ts +55 -0
- package/config/regexAliases.ts +51 -0
- package/config/reserved.ts +1 -1
- package/main.ts +17 -71
- package/package.json +7 -28
- package/plugins/db.ts +11 -10
- package/plugins/redis.ts +5 -9
- package/scripts/syncDb/apply.ts +3 -3
- package/scripts/syncDb/constants.ts +2 -1
- package/scripts/syncDb/ddl.ts +15 -8
- package/scripts/syncDb/helpers.ts +3 -2
- package/scripts/syncDb/index.ts +23 -35
- package/scripts/syncDb/state.ts +8 -6
- package/scripts/syncDb/table.ts +32 -22
- package/scripts/syncDb/tableCreate.ts +9 -3
- package/scripts/syncDb/tests/constants.test.ts +2 -1
- package/scripts/syncDb.ts +10 -9
- package/types/addon.d.ts +53 -0
- package/types/api.d.ts +17 -14
- package/types/befly.d.ts +2 -6
- package/types/context.d.ts +7 -0
- package/types/database.d.ts +9 -14
- package/types/index.d.ts +442 -8
- package/types/index.ts +35 -56
- package/types/redis.d.ts +2 -0
- package/types/validator.d.ts +0 -2
- package/types/validator.ts +43 -0
- package/utils/colors.ts +117 -37
- package/utils/database.ts +348 -0
- package/utils/dbHelper.ts +687 -116
- package/utils/helper.ts +812 -0
- package/utils/index.ts +10 -23
- package/utils/logger.ts +78 -171
- package/utils/redisHelper.ts +135 -152
- package/{types/context.ts → utils/requestContext.ts} +3 -3
- package/utils/sqlBuilder.ts +142 -165
- package/utils/validate.ts +51 -9
- package/apis/health/info.ts +0 -64
- package/apis/tool/tokenCheck.ts +0 -51
- package/bin/befly.ts +0 -202
- package/bunfig.toml +0 -3
- package/plugins/tool.ts +0 -34
- package/scripts/syncDev.ts +0 -112
- package/system.ts +0 -149
- package/tables/_common.json +0 -21
- package/tables/admin.json +0 -10
- package/utils/addonHelper.ts +0 -60
- package/utils/api.ts +0 -23
- package/utils/datetime.ts +0 -51
- package/utils/errorHandler.ts +0 -68
- package/utils/objectHelper.ts +0 -68
- package/utils/pluginHelper.ts +0 -62
- package/utils/response.ts +0 -38
- package/utils/sqlHelper.ts +0 -447
- package/utils/tableHelper.ts +0 -167
- package/utils/tool.ts +0 -230
- package/utils/typeHelper.ts +0 -101
package/utils/sqlBuilder.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SQL
|
|
3
|
-
*
|
|
2
|
+
* SQL 构造器 - TypeScript 版本
|
|
3
|
+
* 提供链式 API 构建 SQL 查询
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type {
|
|
7
|
-
import type { SqlQuery, WhereOperator, WhereConditions, InsertData, UpdateData } from '../types/database';
|
|
6
|
+
import type { WhereConditions, SqlValue, OrderByField } from '../types/common.js';
|
|
8
7
|
|
|
9
8
|
/**
|
|
10
9
|
* SQL 构建器类
|
|
@@ -113,7 +112,120 @@ export class SqlBuilder {
|
|
|
113
112
|
*/
|
|
114
113
|
private _validateParam(value: any): void {
|
|
115
114
|
if (value === undefined) {
|
|
116
|
-
throw new Error(
|
|
115
|
+
throw new Error(`参数值不能为 undefined`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 处理单个操作符条件
|
|
121
|
+
*/
|
|
122
|
+
private _applyOperator(fieldName: string, operator: WhereOperator, value: any): void {
|
|
123
|
+
const escapedField = this._escapeField(fieldName);
|
|
124
|
+
|
|
125
|
+
switch (operator) {
|
|
126
|
+
case '$ne':
|
|
127
|
+
case '$not':
|
|
128
|
+
this._validateParam(value);
|
|
129
|
+
this._where.push(`${escapedField} != ?`);
|
|
130
|
+
this._params.push(value);
|
|
131
|
+
break;
|
|
132
|
+
|
|
133
|
+
case '$in':
|
|
134
|
+
if (!Array.isArray(value)) {
|
|
135
|
+
throw new Error(`$in 操作符的值必须是数组 (operator: ${operator})`);
|
|
136
|
+
}
|
|
137
|
+
if (value.length === 0) {
|
|
138
|
+
throw new Error(`$in 操作符的数组不能为空。提示:空数组会导致查询永远不匹配任何记录,这通常不是预期行为。请检查查询条件或移除该字段。`);
|
|
139
|
+
}
|
|
140
|
+
const placeholders = value.map(() => '?').join(',');
|
|
141
|
+
this._where.push(`${escapedField} IN (${placeholders})`);
|
|
142
|
+
this._params.push(...value);
|
|
143
|
+
break;
|
|
144
|
+
|
|
145
|
+
case '$nin':
|
|
146
|
+
case '$notIn':
|
|
147
|
+
if (!Array.isArray(value)) {
|
|
148
|
+
throw new Error(`$nin/$notIn 操作符的值必须是数组 (operator: ${operator})`);
|
|
149
|
+
}
|
|
150
|
+
if (value.length === 0) {
|
|
151
|
+
throw new Error(`$nin/$notIn 操作符的数组不能为空。提示:空数组会导致查询匹配所有记录,这通常不是预期行为。请检查查询条件或移除该字段。`);
|
|
152
|
+
}
|
|
153
|
+
const placeholders2 = value.map(() => '?').join(',');
|
|
154
|
+
this._where.push(`${escapedField} NOT IN (${placeholders2})`);
|
|
155
|
+
this._params.push(...value);
|
|
156
|
+
break;
|
|
157
|
+
|
|
158
|
+
case '$like':
|
|
159
|
+
this._validateParam(value);
|
|
160
|
+
this._where.push(`${escapedField} LIKE ?`);
|
|
161
|
+
this._params.push(value);
|
|
162
|
+
break;
|
|
163
|
+
|
|
164
|
+
case '$notLike':
|
|
165
|
+
this._validateParam(value);
|
|
166
|
+
this._where.push(`${escapedField} NOT LIKE ?`);
|
|
167
|
+
this._params.push(value);
|
|
168
|
+
break;
|
|
169
|
+
|
|
170
|
+
case '$gt':
|
|
171
|
+
this._validateParam(value);
|
|
172
|
+
this._where.push(`${escapedField} > ?`);
|
|
173
|
+
this._params.push(value);
|
|
174
|
+
break;
|
|
175
|
+
|
|
176
|
+
case '$gte':
|
|
177
|
+
this._validateParam(value);
|
|
178
|
+
this._where.push(`${escapedField} >= ?`);
|
|
179
|
+
this._params.push(value);
|
|
180
|
+
break;
|
|
181
|
+
|
|
182
|
+
case '$lt':
|
|
183
|
+
this._validateParam(value);
|
|
184
|
+
this._where.push(`${escapedField} < ?`);
|
|
185
|
+
this._params.push(value);
|
|
186
|
+
break;
|
|
187
|
+
|
|
188
|
+
case '$lte':
|
|
189
|
+
this._validateParam(value);
|
|
190
|
+
this._where.push(`${escapedField} <= ?`);
|
|
191
|
+
this._params.push(value);
|
|
192
|
+
break;
|
|
193
|
+
|
|
194
|
+
case '$between':
|
|
195
|
+
if (Array.isArray(value) && value.length === 2) {
|
|
196
|
+
this._validateParam(value[0]);
|
|
197
|
+
this._validateParam(value[1]);
|
|
198
|
+
this._where.push(`${escapedField} BETWEEN ? AND ?`);
|
|
199
|
+
this._params.push(value[0], value[1]);
|
|
200
|
+
}
|
|
201
|
+
break;
|
|
202
|
+
|
|
203
|
+
case '$notBetween':
|
|
204
|
+
if (Array.isArray(value) && value.length === 2) {
|
|
205
|
+
this._validateParam(value[0]);
|
|
206
|
+
this._validateParam(value[1]);
|
|
207
|
+
this._where.push(`${escapedField} NOT BETWEEN ? AND ?`);
|
|
208
|
+
this._params.push(value[0], value[1]);
|
|
209
|
+
}
|
|
210
|
+
break;
|
|
211
|
+
|
|
212
|
+
case '$null':
|
|
213
|
+
if (value === true) {
|
|
214
|
+
this._where.push(`${escapedField} IS NULL`);
|
|
215
|
+
}
|
|
216
|
+
break;
|
|
217
|
+
|
|
218
|
+
case '$notNull':
|
|
219
|
+
if (value === true) {
|
|
220
|
+
this._where.push(`${escapedField} IS NOT NULL`);
|
|
221
|
+
}
|
|
222
|
+
break;
|
|
223
|
+
|
|
224
|
+
default:
|
|
225
|
+
// 等于条件
|
|
226
|
+
this._validateParam(value);
|
|
227
|
+
this._where.push(`${escapedField} = ?`);
|
|
228
|
+
this._params.push(value);
|
|
117
229
|
}
|
|
118
230
|
}
|
|
119
231
|
|
|
@@ -158,159 +270,14 @@ export class SqlBuilder {
|
|
|
158
270
|
// 一级属性格式:age$gt, role$in 等
|
|
159
271
|
const lastDollarIndex = key.lastIndexOf('$');
|
|
160
272
|
const fieldName = key.substring(0, lastDollarIndex);
|
|
161
|
-
const escapedFieldName = this._escapeField(fieldName);
|
|
162
273
|
const operator = ('$' + key.substring(lastDollarIndex + 1)) as WhereOperator;
|
|
163
|
-
|
|
164
|
-
switch (operator) {
|
|
165
|
-
case '$ne':
|
|
166
|
-
case '$not':
|
|
167
|
-
this._validateParam(value);
|
|
168
|
-
this._where.push(`${escapedFieldName} != ?`);
|
|
169
|
-
this._params.push(value);
|
|
170
|
-
break;
|
|
171
|
-
case '$in':
|
|
172
|
-
if (Array.isArray(value) && value.length > 0) {
|
|
173
|
-
const placeholders = value.map(() => '?').join(',');
|
|
174
|
-
this._where.push(`${escapedFieldName} IN (${placeholders})`);
|
|
175
|
-
this._params.push(...value);
|
|
176
|
-
}
|
|
177
|
-
break;
|
|
178
|
-
case '$nin':
|
|
179
|
-
case '$notIn':
|
|
180
|
-
if (Array.isArray(value) && value.length > 0) {
|
|
181
|
-
const placeholders = value.map(() => '?').join(',');
|
|
182
|
-
this._where.push(`${escapedFieldName} NOT IN (${placeholders})`);
|
|
183
|
-
this._params.push(...value);
|
|
184
|
-
}
|
|
185
|
-
break;
|
|
186
|
-
case '$like':
|
|
187
|
-
this._validateParam(value);
|
|
188
|
-
this._where.push(`${escapedFieldName} LIKE ?`);
|
|
189
|
-
this._params.push(value);
|
|
190
|
-
break;
|
|
191
|
-
case '$notLike':
|
|
192
|
-
this._validateParam(value);
|
|
193
|
-
this._where.push(`${escapedFieldName} NOT LIKE ?`);
|
|
194
|
-
this._params.push(value);
|
|
195
|
-
break;
|
|
196
|
-
case '$gt':
|
|
197
|
-
this._validateParam(value);
|
|
198
|
-
this._where.push(`${escapedFieldName} > ?`);
|
|
199
|
-
this._params.push(value);
|
|
200
|
-
break;
|
|
201
|
-
case '$gte':
|
|
202
|
-
this._validateParam(value);
|
|
203
|
-
this._where.push(`${escapedFieldName} >= ?`);
|
|
204
|
-
this._params.push(value);
|
|
205
|
-
break;
|
|
206
|
-
case '$lt':
|
|
207
|
-
this._validateParam(value);
|
|
208
|
-
this._where.push(`${escapedFieldName} < ?`);
|
|
209
|
-
this._params.push(value);
|
|
210
|
-
break;
|
|
211
|
-
case '$lte':
|
|
212
|
-
this._validateParam(value);
|
|
213
|
-
this._where.push(`${escapedFieldName} <= ?`);
|
|
214
|
-
this._params.push(value);
|
|
215
|
-
break;
|
|
216
|
-
case '$between':
|
|
217
|
-
if (Array.isArray(value) && value.length === 2) {
|
|
218
|
-
this._where.push(`${escapedFieldName} BETWEEN ? AND ?`);
|
|
219
|
-
this._params.push(value[0], value[1]);
|
|
220
|
-
}
|
|
221
|
-
break;
|
|
222
|
-
case '$notBetween':
|
|
223
|
-
if (Array.isArray(value) && value.length === 2) {
|
|
224
|
-
this._where.push(`${escapedFieldName} NOT BETWEEN ? AND ?`);
|
|
225
|
-
this._params.push(value[0], value[1]);
|
|
226
|
-
}
|
|
227
|
-
break;
|
|
228
|
-
case '$null':
|
|
229
|
-
if (value === true) {
|
|
230
|
-
this._where.push(`${escapedFieldName} IS NULL`);
|
|
231
|
-
}
|
|
232
|
-
break;
|
|
233
|
-
case '$notNull':
|
|
234
|
-
if (value === true) {
|
|
235
|
-
this._where.push(`${escapedFieldName} IS NOT NULL`);
|
|
236
|
-
}
|
|
237
|
-
break;
|
|
238
|
-
default:
|
|
239
|
-
this._validateParam(value);
|
|
240
|
-
this._where.push(`${escapedFieldName} = ?`);
|
|
241
|
-
this._params.push(value);
|
|
242
|
-
}
|
|
274
|
+
this._applyOperator(fieldName, operator, value);
|
|
243
275
|
} else {
|
|
244
276
|
// 检查值是否为对象(嵌套条件)
|
|
245
277
|
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
246
278
|
// 嵌套条件:如 { age: { $gt: 18 } }
|
|
247
|
-
const escapedKey = this._escapeField(key);
|
|
248
279
|
for (const [op, val] of Object.entries(value)) {
|
|
249
|
-
|
|
250
|
-
case '$ne':
|
|
251
|
-
case '$not':
|
|
252
|
-
this._validateParam(val);
|
|
253
|
-
this._where.push(`${escapedKey} != ?`);
|
|
254
|
-
this._params.push(val);
|
|
255
|
-
break;
|
|
256
|
-
case '$gt':
|
|
257
|
-
this._validateParam(val);
|
|
258
|
-
this._where.push(`${escapedKey} > ?`);
|
|
259
|
-
this._params.push(val);
|
|
260
|
-
break;
|
|
261
|
-
case '$gte':
|
|
262
|
-
this._validateParam(val);
|
|
263
|
-
this._where.push(`${escapedKey} >= ?`);
|
|
264
|
-
this._params.push(val);
|
|
265
|
-
break;
|
|
266
|
-
case '$lt':
|
|
267
|
-
this._validateParam(val);
|
|
268
|
-
this._where.push(`${escapedKey} < ?`);
|
|
269
|
-
this._params.push(val);
|
|
270
|
-
break;
|
|
271
|
-
case '$lte':
|
|
272
|
-
this._validateParam(val);
|
|
273
|
-
this._where.push(`${escapedKey} <= ?`);
|
|
274
|
-
this._params.push(val);
|
|
275
|
-
break;
|
|
276
|
-
case '$like':
|
|
277
|
-
this._validateParam(val);
|
|
278
|
-
this._where.push(`${escapedKey} LIKE ?`);
|
|
279
|
-
this._params.push(val);
|
|
280
|
-
break;
|
|
281
|
-
case '$notLike':
|
|
282
|
-
this._validateParam(val);
|
|
283
|
-
this._where.push(`${escapedKey} NOT LIKE ?`);
|
|
284
|
-
this._params.push(val);
|
|
285
|
-
break;
|
|
286
|
-
case '$in':
|
|
287
|
-
if (Array.isArray(val) && val.length > 0) {
|
|
288
|
-
const placeholders = val.map(() => '?').join(',');
|
|
289
|
-
this._where.push(`${escapedKey} IN (${placeholders})`);
|
|
290
|
-
this._params.push(...val);
|
|
291
|
-
}
|
|
292
|
-
break;
|
|
293
|
-
case '$nin':
|
|
294
|
-
case '$notIn':
|
|
295
|
-
if (Array.isArray(val) && val.length > 0) {
|
|
296
|
-
const placeholders = val.map(() => '?').join(',');
|
|
297
|
-
this._where.push(`${escapedKey} NOT IN (${placeholders})`);
|
|
298
|
-
this._params.push(...val);
|
|
299
|
-
}
|
|
300
|
-
break;
|
|
301
|
-
case '$null':
|
|
302
|
-
if (val) {
|
|
303
|
-
this._where.push(`${escapedKey} IS NULL`);
|
|
304
|
-
} else {
|
|
305
|
-
this._where.push(`${escapedKey} IS NOT NULL`);
|
|
306
|
-
}
|
|
307
|
-
break;
|
|
308
|
-
default:
|
|
309
|
-
// 未知操作符,按等于处理
|
|
310
|
-
this._validateParam(val);
|
|
311
|
-
this._where.push(`${escapedKey} = ?`);
|
|
312
|
-
this._params.push(val);
|
|
313
|
-
}
|
|
280
|
+
this._applyOperator(key, op as WhereOperator, val);
|
|
314
281
|
}
|
|
315
282
|
} else {
|
|
316
283
|
// 简单的等于条件
|
|
@@ -323,6 +290,16 @@ export class SqlBuilder {
|
|
|
323
290
|
});
|
|
324
291
|
}
|
|
325
292
|
|
|
293
|
+
/**
|
|
294
|
+
* 获取 WHERE 条件(供 DbHelper 使用)
|
|
295
|
+
*/
|
|
296
|
+
getWhereConditions(): { sql: string; params: SqlValue[] } {
|
|
297
|
+
return {
|
|
298
|
+
sql: this._where.length > 0 ? this._where.join(' AND ') : '',
|
|
299
|
+
params: [...this._params]
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
326
303
|
/**
|
|
327
304
|
* SELECT 字段
|
|
328
305
|
*/
|
|
@@ -342,7 +319,7 @@ export class SqlBuilder {
|
|
|
342
319
|
*/
|
|
343
320
|
from(table: string): this {
|
|
344
321
|
if (typeof table !== 'string' || !table.trim()) {
|
|
345
|
-
throw new Error(
|
|
322
|
+
throw new Error(`FROM 表名必须是非空字符串 (table: ${table})`);
|
|
346
323
|
}
|
|
347
324
|
this._from = this._escapeTable(table.trim());
|
|
348
325
|
return this;
|
|
@@ -370,7 +347,7 @@ export class SqlBuilder {
|
|
|
370
347
|
*/
|
|
371
348
|
leftJoin(table: string, on: string): this {
|
|
372
349
|
if (typeof table !== 'string' || typeof on !== 'string') {
|
|
373
|
-
throw new Error(
|
|
350
|
+
throw new Error(`JOIN 表名和条件必须是字符串 (table: ${table}, on: ${on})`);
|
|
374
351
|
}
|
|
375
352
|
const escapedTable = this._escapeTable(table);
|
|
376
353
|
this._joins.push(`LEFT JOIN ${escapedTable} ON ${on}`);
|
|
@@ -388,7 +365,7 @@ export class SqlBuilder {
|
|
|
388
365
|
|
|
389
366
|
fields.forEach((item) => {
|
|
390
367
|
if (typeof item !== 'string' || !item.includes('#')) {
|
|
391
|
-
throw new Error(
|
|
368
|
+
throw new Error(`orderBy 字段必须是 "字段#方向" 格式的字符串(例如:"name#ASC", "id#DESC") (item: ${item})`);
|
|
392
369
|
}
|
|
393
370
|
|
|
394
371
|
const [fieldName, direction] = item.split('#');
|
|
@@ -396,11 +373,11 @@ export class SqlBuilder {
|
|
|
396
373
|
const cleanDir = direction.trim().toUpperCase() as OrderDirection;
|
|
397
374
|
|
|
398
375
|
if (!cleanField) {
|
|
399
|
-
throw new Error(
|
|
376
|
+
throw new Error(`orderBy 中字段名不能为空 (item: ${item})`);
|
|
400
377
|
}
|
|
401
378
|
|
|
402
379
|
if (!['ASC', 'DESC'].includes(cleanDir)) {
|
|
403
|
-
throw new Error(
|
|
380
|
+
throw new Error(`ORDER BY 方向必须是 ASC 或 DESC (direction: ${cleanDir})`);
|
|
404
381
|
}
|
|
405
382
|
|
|
406
383
|
const escapedField = this._escapeField(cleanField);
|
|
@@ -438,12 +415,12 @@ export class SqlBuilder {
|
|
|
438
415
|
*/
|
|
439
416
|
limit(count: number, offset?: number): this {
|
|
440
417
|
if (typeof count !== 'number' || count < 0) {
|
|
441
|
-
throw new Error(
|
|
418
|
+
throw new Error(`LIMIT 数量必须是非负数 (count: ${count})`);
|
|
442
419
|
}
|
|
443
420
|
this._limit = Math.floor(count);
|
|
444
421
|
if (offset !== undefined && offset !== null) {
|
|
445
422
|
if (typeof offset !== 'number' || offset < 0) {
|
|
446
|
-
throw new Error(
|
|
423
|
+
throw new Error(`OFFSET 必须是非负数 (offset: ${offset})`);
|
|
447
424
|
}
|
|
448
425
|
this._offset = Math.floor(offset);
|
|
449
426
|
}
|
|
@@ -455,7 +432,7 @@ export class SqlBuilder {
|
|
|
455
432
|
*/
|
|
456
433
|
offset(count: number): this {
|
|
457
434
|
if (typeof count !== 'number' || count < 0) {
|
|
458
|
-
throw new Error(
|
|
435
|
+
throw new Error(`OFFSET 必须是非负数 (count: ${count})`);
|
|
459
436
|
}
|
|
460
437
|
this._offset = Math.floor(count);
|
|
461
438
|
return this;
|
|
@@ -509,23 +486,23 @@ export class SqlBuilder {
|
|
|
509
486
|
*/
|
|
510
487
|
toInsertSql(table: string, data: InsertData): SqlQuery {
|
|
511
488
|
if (!table || typeof table !== 'string') {
|
|
512
|
-
throw new Error(
|
|
489
|
+
throw new Error(`INSERT 需要表名 (table: ${table})`);
|
|
513
490
|
}
|
|
514
491
|
|
|
515
492
|
if (!data || typeof data !== 'object') {
|
|
516
|
-
throw new Error(
|
|
493
|
+
throw new Error(`INSERT 需要数据 (table: ${table}, data: ${JSON.stringify(data)})`);
|
|
517
494
|
}
|
|
518
495
|
|
|
519
496
|
const escapedTable = this._escapeTable(table);
|
|
520
497
|
|
|
521
498
|
if (Array.isArray(data)) {
|
|
522
499
|
if (data.length === 0) {
|
|
523
|
-
throw new Error(
|
|
500
|
+
throw new Error(`插入数据不能为空 (table: ${table})`);
|
|
524
501
|
}
|
|
525
502
|
|
|
526
503
|
const fields = Object.keys(data[0]);
|
|
527
504
|
if (fields.length === 0) {
|
|
528
|
-
throw new Error(
|
|
505
|
+
throw new Error(`插入数据必须至少有一个字段 (table: ${table})`);
|
|
529
506
|
}
|
|
530
507
|
|
|
531
508
|
const escapedFields = fields.map((field) => this._escapeField(field));
|
|
@@ -539,7 +516,7 @@ export class SqlBuilder {
|
|
|
539
516
|
} else {
|
|
540
517
|
const fields = Object.keys(data);
|
|
541
518
|
if (fields.length === 0) {
|
|
542
|
-
throw new Error(
|
|
519
|
+
throw new Error(`插入数据必须至少有一个字段 (table: ${table})`);
|
|
543
520
|
}
|
|
544
521
|
|
|
545
522
|
const escapedFields = fields.map((field) => this._escapeField(field));
|
package/utils/validate.ts
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
* 提供类型安全的字段验证功能
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { isType } from './
|
|
7
|
-
import { parseRule } from './
|
|
6
|
+
import { isType } from './helper.js';
|
|
7
|
+
import { parseRule } from './helper.js';
|
|
8
|
+
import { RegexAliases } from '../config/regexAliases.js';
|
|
8
9
|
import type { TableDefinition, FieldRule, ParsedFieldRule } from '../types/common.js';
|
|
9
10
|
import type { ValidationResult, ValidationError } from '../types/validator';
|
|
10
11
|
|
|
@@ -104,12 +105,42 @@ export class Validator {
|
|
|
104
105
|
}
|
|
105
106
|
}
|
|
106
107
|
|
|
108
|
+
/**
|
|
109
|
+
* 解析 regex 别名
|
|
110
|
+
* 如果 regex 以 @ 开头,则从别名表中获取实际正则表达式
|
|
111
|
+
* @param regex - 原始 regex 或别名(如 @number)
|
|
112
|
+
* @returns 实际的正则表达式字符串
|
|
113
|
+
*/
|
|
114
|
+
private resolveRegexAlias(regex: string | null): string | null {
|
|
115
|
+
if (!regex || typeof regex !== 'string') {
|
|
116
|
+
return regex;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 检查是否是别名(以 @ 开头)
|
|
120
|
+
if (regex.startsWith('@')) {
|
|
121
|
+
const aliasName = regex.substring(1);
|
|
122
|
+
const resolvedRegex = RegexAliases[aliasName as keyof typeof RegexAliases];
|
|
123
|
+
|
|
124
|
+
if (resolvedRegex) {
|
|
125
|
+
return resolvedRegex;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 别名不存在,返回原值(让后续验证报错)
|
|
129
|
+
return regex;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return regex;
|
|
133
|
+
}
|
|
134
|
+
|
|
107
135
|
/**
|
|
108
136
|
* 验证单个字段的值
|
|
109
137
|
*/
|
|
110
138
|
private validateFieldValue(value: any, rule: FieldRule, fieldName: string): ValidationError {
|
|
111
139
|
const parsed = parseRule(rule);
|
|
112
|
-
|
|
140
|
+
let { name, type, min, max, regex } = parsed;
|
|
141
|
+
|
|
142
|
+
// 解析 regex 别名
|
|
143
|
+
regex = this.resolveRegexAlias(regex);
|
|
113
144
|
|
|
114
145
|
switch (type.toLowerCase()) {
|
|
115
146
|
case 'number':
|
|
@@ -118,8 +149,9 @@ export class Validator {
|
|
|
118
149
|
return this.validateString(value, name, min, max, regex, fieldName);
|
|
119
150
|
case 'text':
|
|
120
151
|
return this.validateString(value, name, min, max, regex, fieldName);
|
|
121
|
-
case '
|
|
122
|
-
|
|
152
|
+
case 'array_string':
|
|
153
|
+
case 'array_text':
|
|
154
|
+
return this.validateArray(value, name, min, max, regex, fieldName);
|
|
123
155
|
default:
|
|
124
156
|
return `字段 ${fieldName} 的类型 ${type} 不支持`;
|
|
125
157
|
}
|
|
@@ -255,13 +287,22 @@ export class Validator {
|
|
|
255
287
|
*/
|
|
256
288
|
static validateSingleValue(value: any, rule: string): { valid: boolean; value: any; errors: string[] } {
|
|
257
289
|
const parsed = parseRule(rule);
|
|
258
|
-
|
|
290
|
+
let { name, type, min, max, regex, default: defaultValue } = parsed;
|
|
291
|
+
|
|
292
|
+
// 解析 regex 别名
|
|
293
|
+
if (regex && typeof regex === 'string' && regex.startsWith('@')) {
|
|
294
|
+
const aliasName = regex.substring(1);
|
|
295
|
+
const resolvedRegex = RegexAliases[aliasName as keyof typeof RegexAliases];
|
|
296
|
+
if (resolvedRegex) {
|
|
297
|
+
regex = resolvedRegex;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
259
300
|
|
|
260
301
|
// 处理 undefined/null 值,使用默认值
|
|
261
302
|
if (value === undefined || value === null) {
|
|
262
303
|
if (defaultValue !== 'null' && defaultValue !== null) {
|
|
263
304
|
// 特殊处理数组类型的默认值字符串
|
|
264
|
-
if (type === '
|
|
305
|
+
if ((type === 'array_string' || type === 'array_text') && typeof defaultValue === 'string') {
|
|
265
306
|
if (defaultValue === '[]') {
|
|
266
307
|
return { valid: true, value: [], errors: [] };
|
|
267
308
|
}
|
|
@@ -281,7 +322,7 @@ export class Validator {
|
|
|
281
322
|
// 如果没有默认值,根据类型返回默认值
|
|
282
323
|
if (type === 'number') {
|
|
283
324
|
return { valid: true, value: 0, errors: [] };
|
|
284
|
-
} else if (type === '
|
|
325
|
+
} else if (type === 'array_string' || type === 'array_text') {
|
|
285
326
|
return { valid: true, value: [], errors: [] };
|
|
286
327
|
} else if (type === 'string' || type === 'text') {
|
|
287
328
|
return { valid: true, value: '', errors: [] };
|
|
@@ -347,7 +388,8 @@ export class Validator {
|
|
|
347
388
|
}
|
|
348
389
|
break;
|
|
349
390
|
|
|
350
|
-
case '
|
|
391
|
+
case 'array_string':
|
|
392
|
+
case 'array_text':
|
|
351
393
|
if (!Array.isArray(convertedValue)) {
|
|
352
394
|
errors.push(`${name || '值'}必须是数组`);
|
|
353
395
|
}
|
package/apis/health/info.ts
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 健康检查接口 - TypeScript 版本
|
|
3
|
-
* 检查服务器、Redis、数据库等状态
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { Env } from '../../config/env.js';
|
|
7
|
-
import { Api } from '../../utils/api.js';
|
|
8
|
-
import { Yes } from '../../utils/index.js';
|
|
9
|
-
import type { BeflyContext } from '../../types/befly.js';
|
|
10
|
-
import type { HealthInfo } from '../../types/api.js';
|
|
11
|
-
|
|
12
|
-
export default Api('健康检查', {
|
|
13
|
-
fields: {},
|
|
14
|
-
required: [],
|
|
15
|
-
handler: async (befly: BeflyContext, ctx: any) => {
|
|
16
|
-
const info: HealthInfo = {
|
|
17
|
-
status: 'ok',
|
|
18
|
-
timestamp: new Date().toISOString(),
|
|
19
|
-
uptime: process.uptime(),
|
|
20
|
-
memory: process.memoryUsage(),
|
|
21
|
-
runtime: 'Bun',
|
|
22
|
-
version: Bun.version,
|
|
23
|
-
platform: process.platform,
|
|
24
|
-
arch: process.arch
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
// 检查 Redis 连接状态
|
|
28
|
-
if (Env.REDIS_ENABLE === 1) {
|
|
29
|
-
if (befly.redis) {
|
|
30
|
-
try {
|
|
31
|
-
await befly.redis.getRedisClient().ping();
|
|
32
|
-
info.redis = '已连接';
|
|
33
|
-
} catch (error: any) {
|
|
34
|
-
info.redis = '未连接';
|
|
35
|
-
info.redisError = error.message;
|
|
36
|
-
}
|
|
37
|
-
} else {
|
|
38
|
-
info.redis = '未开启';
|
|
39
|
-
}
|
|
40
|
-
} else {
|
|
41
|
-
info.redis = '禁用';
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// 检查数据库连接状态
|
|
45
|
-
if (Env.DB_ENABLE === 1) {
|
|
46
|
-
if (befly.db) {
|
|
47
|
-
try {
|
|
48
|
-
// 执行简单查询测试连接
|
|
49
|
-
await befly.db.query('SELECT 1');
|
|
50
|
-
info.database = '已连接';
|
|
51
|
-
} catch (error: any) {
|
|
52
|
-
info.database = '未连接';
|
|
53
|
-
info.databaseError = error.message;
|
|
54
|
-
}
|
|
55
|
-
} else {
|
|
56
|
-
info.database = '未开启';
|
|
57
|
-
}
|
|
58
|
-
} else {
|
|
59
|
-
info.database = '禁用';
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return Yes('健康检查成功', info);
|
|
63
|
-
}
|
|
64
|
-
});
|
package/apis/tool/tokenCheck.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 令牌检测接口 - TypeScript 版本
|
|
3
|
-
* 验证 JWT 令牌是否有效
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { Env } from '../../config/env.js';
|
|
7
|
-
import { Api } from '../../utils/api.js';
|
|
8
|
-
import { Yes, No } from '../../utils/index.js';
|
|
9
|
-
import { Jwt } from '../../utils/jwt.js';
|
|
10
|
-
import type { BeflyContext } from '../../types/befly.js';
|
|
11
|
-
import type { JwtPayload } from '../../utils/jwt.js';
|
|
12
|
-
import type { TokenCheckData } from '../../types/api.js';
|
|
13
|
-
|
|
14
|
-
export default Api('令牌检测', {
|
|
15
|
-
fields: {},
|
|
16
|
-
required: [],
|
|
17
|
-
handler: async (befly: BeflyContext, ctx: any) => {
|
|
18
|
-
// 从 Authorization 头获取 token
|
|
19
|
-
const authHeader = ctx.req?.headers?.get('authorization') || '';
|
|
20
|
-
const token = authHeader.split(' ')[1] || '';
|
|
21
|
-
|
|
22
|
-
if (!token) {
|
|
23
|
-
return No('令牌不能为空');
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
try {
|
|
27
|
-
// 验证令牌
|
|
28
|
-
const jwtData = await Jwt.verify(token);
|
|
29
|
-
|
|
30
|
-
// 计算剩余有效期
|
|
31
|
-
const expiresIn = jwtData.exp ? jwtData.exp - Math.floor(Date.now() / 1000) : undefined;
|
|
32
|
-
|
|
33
|
-
const data: TokenCheckData = {
|
|
34
|
-
valid: true,
|
|
35
|
-
payload: jwtData,
|
|
36
|
-
expiresIn
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
return Yes('令牌有效', data);
|
|
40
|
-
} catch (error: any) {
|
|
41
|
-
// 针对预期的令牌错误,返回友好提示(非致命错误)
|
|
42
|
-
if (error.message.includes('expired')) {
|
|
43
|
-
return No('令牌已过期', { expired: true });
|
|
44
|
-
} else if (error.message.includes('invalid')) {
|
|
45
|
-
return No('令牌无效', { invalid: true });
|
|
46
|
-
}
|
|
47
|
-
// 其他未知错误向上抛出,由路由层统一处理
|
|
48
|
-
throw error;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
});
|