befly 3.17.0 → 3.17.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/apis/admin/cacheRefresh.js +122 -0
- package/apis/admin/del.js +34 -0
- package/apis/admin/detail.js +23 -0
- package/apis/admin/ins.js +69 -0
- package/apis/admin/list.js +28 -0
- package/apis/admin/upd.js +95 -0
- package/apis/api/all.js +24 -0
- package/apis/api/list.js +31 -0
- package/apis/auth/login.js +123 -0
- package/apis/auth/sendSmsCode.js +24 -0
- package/apis/dashboard/configStatus.js +39 -0
- package/apis/dashboard/environmentInfo.js +43 -0
- package/apis/dashboard/performanceMetrics.js +20 -0
- package/apis/dashboard/permissionStats.js +27 -0
- package/apis/dashboard/serviceStatus.js +75 -0
- package/apis/dashboard/systemInfo.js +19 -0
- package/apis/dashboard/systemOverview.js +30 -0
- package/apis/dashboard/systemResources.js +106 -0
- package/apis/dict/all.js +23 -0
- package/apis/dict/del.js +16 -0
- package/apis/dict/detail.js +27 -0
- package/apis/dict/ins.js +51 -0
- package/apis/dict/items.js +30 -0
- package/apis/dict/list.js +36 -0
- package/apis/dict/upd.js +74 -0
- package/apis/dictType/all.js +16 -0
- package/apis/dictType/del.js +38 -0
- package/apis/dictType/detail.js +20 -0
- package/apis/dictType/ins.js +37 -0
- package/apis/dictType/list.js +26 -0
- package/apis/dictType/upd.js +51 -0
- package/apis/email/config.js +25 -0
- package/apis/email/logList.js +23 -0
- package/apis/email/send.js +66 -0
- package/apis/email/verify.js +21 -0
- package/apis/loginLog/list.js +23 -0
- package/apis/menu/all.js +41 -0
- package/apis/menu/list.js +25 -0
- package/apis/operateLog/list.js +23 -0
- package/apis/role/all.js +21 -0
- package/apis/role/apiSave.js +43 -0
- package/apis/role/apis.js +22 -0
- package/apis/role/del.js +49 -0
- package/apis/role/detail.js +32 -0
- package/apis/role/ins.js +46 -0
- package/apis/role/list.js +27 -0
- package/apis/role/menuSave.js +42 -0
- package/apis/role/menus.js +22 -0
- package/apis/role/save.js +40 -0
- package/apis/role/upd.js +50 -0
- package/apis/sysConfig/all.js +16 -0
- package/apis/sysConfig/del.js +36 -0
- package/apis/sysConfig/get.js +49 -0
- package/apis/sysConfig/ins.js +50 -0
- package/apis/sysConfig/list.js +24 -0
- package/apis/sysConfig/upd.js +62 -0
- package/checks/api.js +55 -0
- package/checks/config.js +107 -0
- package/checks/hook.js +38 -0
- package/checks/menu.js +58 -0
- package/checks/plugin.js +38 -0
- package/checks/table.js +78 -0
- package/configs/beflyConfig.json +61 -0
- package/configs/beflyMenus.json +85 -0
- package/configs/constConfig.js +34 -0
- package/configs/regexpAlias.json +55 -0
- package/hooks/auth.js +34 -0
- package/hooks/cors.js +39 -0
- package/hooks/parser.js +90 -0
- package/hooks/permission.js +71 -0
- package/hooks/validator.js +43 -0
- package/index.js +326 -0
- package/lib/cacheHelper.js +483 -0
- package/lib/cacheKeys.js +42 -0
- package/lib/connect.js +120 -0
- package/lib/dbHelper/builders.js +698 -0
- package/lib/dbHelper/context.js +131 -0
- package/lib/dbHelper/dataOps.js +505 -0
- package/lib/dbHelper/execute.js +65 -0
- package/lib/dbHelper/index.js +27 -0
- package/lib/dbHelper/transaction.js +43 -0
- package/lib/dbHelper/util.js +58 -0
- package/lib/dbHelper/validate.js +549 -0
- package/lib/emailHelper.js +110 -0
- package/lib/logger.js +604 -0
- package/lib/redisHelper.js +684 -0
- package/lib/sqlBuilder/batch.js +113 -0
- package/lib/sqlBuilder/check.js +150 -0
- package/lib/sqlBuilder/compiler.js +347 -0
- package/lib/sqlBuilder/errors.js +60 -0
- package/lib/sqlBuilder/index.js +218 -0
- package/lib/sqlBuilder/parser.js +296 -0
- package/lib/sqlBuilder/util.js +260 -0
- package/lib/validator.js +303 -0
- package/package.json +19 -12
- package/paths.js +112 -0
- package/plugins/cache.js +16 -0
- package/plugins/config.js +11 -0
- package/plugins/email.js +27 -0
- package/plugins/logger.js +20 -0
- package/plugins/mysql.js +36 -0
- package/plugins/redis.js +34 -0
- package/plugins/tool.js +155 -0
- package/router/api.js +140 -0
- package/router/static.js +71 -0
- package/sql/admin.sql +18 -0
- package/sql/api.sql +12 -0
- package/sql/dict.sql +13 -0
- package/sql/dictType.sql +12 -0
- package/sql/emailLog.sql +20 -0
- package/sql/loginLog.sql +25 -0
- package/sql/menu.sql +12 -0
- package/sql/operateLog.sql +22 -0
- package/sql/role.sql +14 -0
- package/sql/sysConfig.sql +16 -0
- package/sync/api.js +93 -0
- package/sync/cache.js +13 -0
- package/sync/dev.js +171 -0
- package/sync/menu.js +99 -0
- package/tables/admin.json +56 -0
- package/tables/api.json +26 -0
- package/tables/dict.json +30 -0
- package/tables/dictType.json +24 -0
- package/tables/emailLog.json +61 -0
- package/tables/loginLog.json +86 -0
- package/tables/menu.json +24 -0
- package/tables/operateLog.json +68 -0
- package/tables/role.json +32 -0
- package/tables/sysConfig.json +43 -0
- package/utils/calcPerfTime.js +13 -0
- package/utils/cors.js +17 -0
- package/utils/deepMerge.js +78 -0
- package/utils/fieldClear.js +65 -0
- package/utils/formatYmdHms.js +23 -0
- package/utils/formatZodIssues.js +109 -0
- package/utils/getClientIp.js +47 -0
- package/utils/importDefault.js +51 -0
- package/utils/is.js +462 -0
- package/utils/loggerUtils.js +185 -0
- package/utils/processInfo.js +39 -0
- package/utils/regexpUtil.js +52 -0
- package/utils/response.js +114 -0
- package/utils/scanFiles.js +124 -0
- package/utils/scanSources.js +68 -0
- package/utils/sortModules.js +75 -0
- package/utils/toSessionTtlSeconds.js +14 -0
- package/utils/util.js +374 -0
- package/befly.js +0 -16413
- package/befly.min.js +0 -72
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { isNonEmptyString, isNullable, isNumber, isPlainObject, isString } from "../../utils/is.js";
|
|
2
|
+
import { canConvertToNumber } from "../../utils/util.js";
|
|
3
|
+
|
|
4
|
+
export function quoteIdentMySql(identifier) {
|
|
5
|
+
if (!isString(identifier)) {
|
|
6
|
+
throw new Error(`quoteIdentifier 需要字符串类型标识符 (identifier: ${String(identifier)})`, {
|
|
7
|
+
cause: null,
|
|
8
|
+
code: "validation"
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const trimmed = identifier.trim();
|
|
13
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(trimmed)) {
|
|
14
|
+
throw new Error(`无效的 SQL 标识符: ${trimmed}`, {
|
|
15
|
+
cause: null,
|
|
16
|
+
code: "validation"
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return `\`${trimmed}\``;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function hasBegin(sql) {
|
|
24
|
+
return typeof sql.begin === "function";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class DbSqlError extends Error {
|
|
28
|
+
constructor(message, options) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.originalError = options.originalError;
|
|
31
|
+
this.params = options.params;
|
|
32
|
+
this.duration = options.duration;
|
|
33
|
+
this.sqlInfo = options.sqlInfo;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class TransAbortError extends Error {
|
|
38
|
+
constructor(payload) {
|
|
39
|
+
super("TRANSACTION_ABORT");
|
|
40
|
+
this.payload = payload;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function isBeflyResponse(value) {
|
|
45
|
+
if (!isPlainObject(value)) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const record = value;
|
|
50
|
+
return isNumber(record["code"]) && isString(record["msg"]);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function convertBigIntFields(arr, fields) {
|
|
54
|
+
if (isNullable(arr)) {
|
|
55
|
+
return arr;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const defaultFields = ["id", "pid", "sort"];
|
|
59
|
+
|
|
60
|
+
const buildFields = (userFields) => {
|
|
61
|
+
if (!userFields || userFields.length === 0) {
|
|
62
|
+
return defaultFields;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const merged = ["id", "pid", "sort"];
|
|
66
|
+
for (const f of userFields) {
|
|
67
|
+
if (!isString(f)) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (!isNonEmptyString(f)) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const trimmed = f.trim();
|
|
74
|
+
if (!merged.includes(trimmed)) {
|
|
75
|
+
merged.push(trimmed);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return merged;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const effectiveFields = buildFields(fields);
|
|
82
|
+
const fieldSet = new Set(effectiveFields);
|
|
83
|
+
|
|
84
|
+
const convertRecord = (source) => {
|
|
85
|
+
const converted = {};
|
|
86
|
+
|
|
87
|
+
for (const [key, value] of Object.entries(source)) {
|
|
88
|
+
let nextValue = value;
|
|
89
|
+
|
|
90
|
+
if (!isNullable(value)) {
|
|
91
|
+
const shouldConvert = fieldSet.has(key) || key.endsWith("Id") || key.endsWith("_id") || key.endsWith("At") || key.endsWith("_at");
|
|
92
|
+
|
|
93
|
+
if (shouldConvert) {
|
|
94
|
+
let bigintValue = null;
|
|
95
|
+
if (typeof value === "bigint") {
|
|
96
|
+
bigintValue = value;
|
|
97
|
+
} else if (isString(value)) {
|
|
98
|
+
if (/^-?\d+$/.test(value)) {
|
|
99
|
+
try {
|
|
100
|
+
bigintValue = BigInt(value);
|
|
101
|
+
} catch {
|
|
102
|
+
bigintValue = null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (bigintValue !== null) {
|
|
108
|
+
const convertedNumber = canConvertToNumber(bigintValue);
|
|
109
|
+
if (convertedNumber !== null) {
|
|
110
|
+
nextValue = convertedNumber;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
converted[key] = nextValue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return converted;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
if (Array.isArray(arr)) {
|
|
123
|
+
return arr.map((item) => convertRecord(item));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (typeof arr === "object") {
|
|
127
|
+
return convertRecord(arr);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return arr;
|
|
131
|
+
}
|
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
import { isNullable, isNumber } from "../../utils/is.js";
|
|
2
|
+
import { snakeCase } from "../../utils/util.js";
|
|
3
|
+
import { Logger } from "../logger.js";
|
|
4
|
+
import { SqlBuilder } from "../sqlBuilder/index.js";
|
|
5
|
+
import { quoteIdentMySql } from "./context.js";
|
|
6
|
+
import { toNumberFromSql } from "./util.js";
|
|
7
|
+
import { addDefaultStateFilter, buildInsertRow, buildPartialUpdateData, buildUpdateRow, clearDeep, whereKeysToSnake } from "./builders.js";
|
|
8
|
+
import { assertBatchInsertRowsConsistent, assertNoUndefinedInRecord, validateGeneratedBatchId, validateIncrementOptions, validateInsertBatchSize, validateNoJoinReadOptions, validatePageLimitRange, validateSafeFieldName, validateTableBatchDataOptions, validateTableDataOptions, validateTableName, validateTableWhereOptions } from "./validate.js";
|
|
9
|
+
|
|
10
|
+
export const dataOpsMethods = {
|
|
11
|
+
// 读取操作
|
|
12
|
+
async getCount(options) {
|
|
13
|
+
const { table, where, joins, tableQualifier } = await this.prepareQueryOptions(options, "getCount.options");
|
|
14
|
+
const hasJoins = Array.isArray(joins) && joins.length > 0;
|
|
15
|
+
|
|
16
|
+
const whereFiltered = addDefaultStateFilter(where, tableQualifier, hasJoins);
|
|
17
|
+
const result = await this.fetchCount({ table: table, joins: joins }, whereFiltered, "COUNT(*) as count");
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
data: result.total,
|
|
21
|
+
sql: result.sql
|
|
22
|
+
};
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
async getOne(options) {
|
|
26
|
+
const { table, fields, where, joins, tableQualifier } = await this.prepareQueryOptions(options, "getOne.options");
|
|
27
|
+
const hasJoins = Array.isArray(joins) && joins.length > 0;
|
|
28
|
+
|
|
29
|
+
const whereFiltered = addDefaultStateFilter(where, tableQualifier, hasJoins);
|
|
30
|
+
const builder = this.createSqlBuilder().select(fields).from(table).where(whereFiltered);
|
|
31
|
+
this.applyJoins(builder, joins);
|
|
32
|
+
|
|
33
|
+
const { sql, params } = builder.toSelectSql();
|
|
34
|
+
const executeRes = await this.execute(sql, params);
|
|
35
|
+
const result = executeRes.data;
|
|
36
|
+
|
|
37
|
+
const data = this.normalizeRowData(result?.[0] || null, options.bigint);
|
|
38
|
+
return {
|
|
39
|
+
data: data,
|
|
40
|
+
sql: executeRes.sql
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
async getDetail(options) {
|
|
45
|
+
return await this.getOne(options);
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
async getList(options) {
|
|
49
|
+
const prepared = await this.prepareQueryOptions(options, "getList.options");
|
|
50
|
+
validatePageLimitRange(prepared, options.table);
|
|
51
|
+
|
|
52
|
+
const hasJoins = Array.isArray(prepared.joins) && prepared.joins.length > 0;
|
|
53
|
+
const whereFiltered = addDefaultStateFilter(prepared.where, prepared.tableQualifier, hasJoins);
|
|
54
|
+
const countResult = await this.fetchCount(prepared, whereFiltered, "COUNT(*) as total");
|
|
55
|
+
const total = countResult.total;
|
|
56
|
+
|
|
57
|
+
const offset = (prepared.page - 1) * prepared.limit;
|
|
58
|
+
const dataBuilder = this.createListBuilder(prepared, whereFiltered, prepared.limit, offset);
|
|
59
|
+
const { sql: dataSql, params: dataParams } = dataBuilder.toSelectSql();
|
|
60
|
+
|
|
61
|
+
if (total === 0) {
|
|
62
|
+
return {
|
|
63
|
+
data: {
|
|
64
|
+
lists: [],
|
|
65
|
+
total: 0,
|
|
66
|
+
page: prepared.page,
|
|
67
|
+
limit: prepared.limit,
|
|
68
|
+
pages: 0
|
|
69
|
+
},
|
|
70
|
+
sql: {
|
|
71
|
+
count: countResult.sql,
|
|
72
|
+
data: {
|
|
73
|
+
sql: dataSql,
|
|
74
|
+
params: dataParams,
|
|
75
|
+
duration: 0
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const listExecuteRes = await this.execute(dataSql, dataParams);
|
|
81
|
+
const list = listExecuteRes.data || [];
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
data: {
|
|
85
|
+
lists: this.normalizeListData(list, options.bigint),
|
|
86
|
+
total: total,
|
|
87
|
+
page: prepared.page,
|
|
88
|
+
limit: prepared.limit,
|
|
89
|
+
pages: Math.ceil(total / prepared.limit)
|
|
90
|
+
},
|
|
91
|
+
sql: {
|
|
92
|
+
count: countResult.sql,
|
|
93
|
+
data: listExecuteRes.sql
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
async getAll(options) {
|
|
99
|
+
const MAX_LIMIT = 10000;
|
|
100
|
+
const WARNING_LIMIT = 1000;
|
|
101
|
+
const prepareOptions = this.buildQueryOptions(options, { page: 1, limit: 10 });
|
|
102
|
+
const prepared = await this.prepareQueryOptions(prepareOptions, "getAll.options");
|
|
103
|
+
|
|
104
|
+
const hasJoins = Array.isArray(prepared.joins) && prepared.joins.length > 0;
|
|
105
|
+
const whereFiltered = addDefaultStateFilter(prepared.where, prepared.tableQualifier, hasJoins);
|
|
106
|
+
const countResult = await this.fetchCount(prepared, whereFiltered, "COUNT(*) as total");
|
|
107
|
+
const total = countResult.total;
|
|
108
|
+
|
|
109
|
+
if (total === 0) {
|
|
110
|
+
return {
|
|
111
|
+
data: {
|
|
112
|
+
lists: [],
|
|
113
|
+
total: 0
|
|
114
|
+
},
|
|
115
|
+
sql: {
|
|
116
|
+
count: countResult.sql
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const dataBuilder = this.createListBuilder(prepared, whereFiltered, MAX_LIMIT, null);
|
|
122
|
+
|
|
123
|
+
const { sql: dataSql, params: dataParams } = dataBuilder.toSelectSql();
|
|
124
|
+
const listExecuteRes = await this.execute(dataSql, dataParams);
|
|
125
|
+
const result = listExecuteRes.data || [];
|
|
126
|
+
|
|
127
|
+
if (result.length >= WARNING_LIMIT) {
|
|
128
|
+
Logger.warn("getAll 返回数据过多,建议使用 getList 分页查询", { table: options.table, count: result.length, total: total });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (result.length >= MAX_LIMIT) {
|
|
132
|
+
Logger.warn(`getAll 达到最大限制 ${MAX_LIMIT},实际总数 ${total},只返回前 ${MAX_LIMIT} 条`, { table: options.table, limit: MAX_LIMIT, total: total });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const lists = this.normalizeListData(result, options.bigint);
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
data: {
|
|
139
|
+
lists: lists,
|
|
140
|
+
total: total
|
|
141
|
+
},
|
|
142
|
+
sql: {
|
|
143
|
+
count: countResult.sql,
|
|
144
|
+
data: listExecuteRes.sql
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
async exists(options) {
|
|
150
|
+
validateNoJoinReadOptions(options, "exists", "exists 不支持 joins(请使用显式 query 或拆分查询)");
|
|
151
|
+
const snakeTable = snakeCase(options.table);
|
|
152
|
+
const snakeWhere = whereKeysToSnake(clearDeep(options.where || {}));
|
|
153
|
+
const whereFiltered = addDefaultStateFilter(snakeWhere, snakeTable, false);
|
|
154
|
+
|
|
155
|
+
const builder = this.createSqlBuilder().selectRaw("COUNT(1) as cnt").from(snakeTable).where(whereFiltered).limit(1);
|
|
156
|
+
const { sql, params } = builder.toSelectSql();
|
|
157
|
+
const executeRes = await this.execute(sql, params);
|
|
158
|
+
const exists = (executeRes.data?.[0]?.cnt || 0) > 0;
|
|
159
|
+
return { data: exists, sql: executeRes.sql };
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
async getFieldValue(options) {
|
|
163
|
+
validateNoJoinReadOptions(options, "getFieldValue", "getFieldValue 不支持 joins(请使用 getOne/getList 并自行取字段)");
|
|
164
|
+
const field = options.field;
|
|
165
|
+
validateSafeFieldName(field);
|
|
166
|
+
|
|
167
|
+
const oneOptions = {
|
|
168
|
+
table: options.table
|
|
169
|
+
};
|
|
170
|
+
if (options.where !== undefined) oneOptions.where = options.where;
|
|
171
|
+
if (options.joins !== undefined) oneOptions.joins = options.joins;
|
|
172
|
+
if (options.orderBy !== undefined) oneOptions.orderBy = options.orderBy;
|
|
173
|
+
if (options.page !== undefined) oneOptions.page = options.page;
|
|
174
|
+
if (options.limit !== undefined) oneOptions.limit = options.limit;
|
|
175
|
+
oneOptions.fields = [field];
|
|
176
|
+
|
|
177
|
+
const oneRes = await this.getOne(oneOptions);
|
|
178
|
+
|
|
179
|
+
const result = oneRes.data;
|
|
180
|
+
const value = this.resolveFieldValue(result, field);
|
|
181
|
+
return {
|
|
182
|
+
data: value,
|
|
183
|
+
sql: oneRes.sql
|
|
184
|
+
};
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
// 写入操作
|
|
188
|
+
|
|
189
|
+
async insData(options) {
|
|
190
|
+
validateTableDataOptions(options, "insData");
|
|
191
|
+
const { table, data } = options;
|
|
192
|
+
const snakeTable = snakeCase(table);
|
|
193
|
+
const now = Date.now();
|
|
194
|
+
|
|
195
|
+
let processed;
|
|
196
|
+
if (this.idMode === "autoId") {
|
|
197
|
+
processed = buildInsertRow({ idMode: "autoId", data: data, now: now });
|
|
198
|
+
} else {
|
|
199
|
+
let id;
|
|
200
|
+
try {
|
|
201
|
+
id = await this.redis.genTimeID();
|
|
202
|
+
} catch (error) {
|
|
203
|
+
throw new Error(`生成 ID 失败,Redis 可能不可用 (table: ${table})`, {
|
|
204
|
+
cause: error,
|
|
205
|
+
code: "runtime",
|
|
206
|
+
subsystem: "db",
|
|
207
|
+
operation: "genTimeId",
|
|
208
|
+
table: table
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
processed = buildInsertRow({ idMode: "timeId", data: data, id: id, now: now });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
assertNoUndefinedInRecord(processed, `insData 插入数据 (table: ${snakeTable})`);
|
|
215
|
+
|
|
216
|
+
const builder = this.createSqlBuilder();
|
|
217
|
+
const { sql, params } = builder.toInsertSql(snakeTable, processed);
|
|
218
|
+
const executeRes = await this.execute(sql, params);
|
|
219
|
+
|
|
220
|
+
const processedId = processed["id"];
|
|
221
|
+
const processedIdNum = isNumber(processedId) ? processedId : 0;
|
|
222
|
+
const lastInsertRowidNum = toNumberFromSql(executeRes.data?.lastInsertRowid);
|
|
223
|
+
|
|
224
|
+
const insertedId = this.idMode === "autoId" ? lastInsertRowidNum || 0 : processedIdNum || lastInsertRowidNum || 0;
|
|
225
|
+
if (this.idMode === "autoId" && insertedId <= 0) {
|
|
226
|
+
throw new Error(`插入失败:autoId 模式下无法获取 lastInsertRowid (table: ${table})`, {
|
|
227
|
+
cause: null,
|
|
228
|
+
code: "runtime"
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
data: insertedId,
|
|
233
|
+
sql: executeRes.sql
|
|
234
|
+
};
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
async insBatch(table, dataList) {
|
|
238
|
+
validateTableBatchDataOptions(table, dataList, "insBatch");
|
|
239
|
+
if (dataList.length === 0) {
|
|
240
|
+
return {
|
|
241
|
+
data: [],
|
|
242
|
+
sql: { sql: "", params: [], duration: 0 }
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const MAX_BATCH_SIZE = 1000;
|
|
247
|
+
validateInsertBatchSize(dataList.length, MAX_BATCH_SIZE);
|
|
248
|
+
|
|
249
|
+
const snakeTable = snakeCase(table);
|
|
250
|
+
const now = Date.now();
|
|
251
|
+
let ids = [];
|
|
252
|
+
|
|
253
|
+
let processedList;
|
|
254
|
+
if (this.idMode === "autoId") {
|
|
255
|
+
processedList = dataList.map((data) => {
|
|
256
|
+
return buildInsertRow({ idMode: "autoId", data: data, now: now });
|
|
257
|
+
});
|
|
258
|
+
} else {
|
|
259
|
+
const nextIds = [];
|
|
260
|
+
for (let i = 0; i < dataList.length; i++) {
|
|
261
|
+
nextIds.push(await this.redis.genTimeID());
|
|
262
|
+
}
|
|
263
|
+
ids = nextIds;
|
|
264
|
+
processedList = dataList.map((data, index) => {
|
|
265
|
+
const id = nextIds[index];
|
|
266
|
+
validateGeneratedBatchId(id, snakeTable, index);
|
|
267
|
+
return buildInsertRow({ idMode: "timeId", data: data, id: id, now: now });
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const insertFields = assertBatchInsertRowsConsistent(processedList, { table: snakeTable });
|
|
272
|
+
const builder = this.createSqlBuilder();
|
|
273
|
+
const { sql, params } = builder.toInsertSql(snakeTable, processedList);
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
const executeRes = await this.execute(sql, params);
|
|
277
|
+
|
|
278
|
+
if (this.idMode === "autoId") {
|
|
279
|
+
const firstId = toNumberFromSql(executeRes.data?.lastInsertRowid);
|
|
280
|
+
if (firstId <= 0) {
|
|
281
|
+
throw new Error(`批量插入失败:autoId 模式下无法获取 lastInsertRowid (table: ${table})`, {
|
|
282
|
+
cause: null,
|
|
283
|
+
code: "runtime"
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const outIds = [];
|
|
288
|
+
for (let i = 0; i < dataList.length; i++) {
|
|
289
|
+
outIds.push(firstId + i);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
data: outIds,
|
|
294
|
+
sql: executeRes.sql
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
data: ids,
|
|
300
|
+
sql: executeRes.sql
|
|
301
|
+
};
|
|
302
|
+
} catch (error) {
|
|
303
|
+
Logger.error("批量插入失败", error, {
|
|
304
|
+
table: table,
|
|
305
|
+
snakeTable: snakeTable,
|
|
306
|
+
count: dataList.length,
|
|
307
|
+
fields: insertFields
|
|
308
|
+
});
|
|
309
|
+
throw new Error("批量插入失败", {
|
|
310
|
+
cause: error,
|
|
311
|
+
code: "runtime",
|
|
312
|
+
subsystem: "sql",
|
|
313
|
+
operation: "insBatch",
|
|
314
|
+
table: table,
|
|
315
|
+
snakeTable: snakeTable,
|
|
316
|
+
count: dataList.length,
|
|
317
|
+
fields: insertFields
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
async delForceBatch(table, ids) {
|
|
323
|
+
validateTableName(table, "delForceBatch.table");
|
|
324
|
+
if (ids.length === 0) {
|
|
325
|
+
return {
|
|
326
|
+
data: 0,
|
|
327
|
+
sql: { sql: "", params: [], duration: 0 }
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const snakeTable = snakeCase(table);
|
|
332
|
+
|
|
333
|
+
const query = SqlBuilder.toDeleteInSql({
|
|
334
|
+
table: snakeTable,
|
|
335
|
+
idField: "id",
|
|
336
|
+
ids: ids,
|
|
337
|
+
quoteIdent: quoteIdentMySql
|
|
338
|
+
});
|
|
339
|
+
const executeRes = await this.execute(query.sql, query.params);
|
|
340
|
+
const changes = toNumberFromSql(executeRes.data?.changes);
|
|
341
|
+
return {
|
|
342
|
+
data: changes,
|
|
343
|
+
sql: executeRes.sql
|
|
344
|
+
};
|
|
345
|
+
},
|
|
346
|
+
|
|
347
|
+
async updBatch(table, dataList) {
|
|
348
|
+
validateTableBatchDataOptions(table, dataList, "updBatch");
|
|
349
|
+
if (dataList.length === 0) {
|
|
350
|
+
return {
|
|
351
|
+
data: 0,
|
|
352
|
+
sql: { sql: "", params: [], duration: 0 }
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const snakeTable = snakeCase(table);
|
|
357
|
+
const now = Date.now();
|
|
358
|
+
|
|
359
|
+
const processedList = [];
|
|
360
|
+
const fieldSet = new Set();
|
|
361
|
+
|
|
362
|
+
for (const item of dataList) {
|
|
363
|
+
const userData = buildPartialUpdateData({ data: item.data, allowState: true });
|
|
364
|
+
|
|
365
|
+
for (const key of Object.keys(userData)) {
|
|
366
|
+
fieldSet.add(key);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
processedList.push({ id: item.id, data: userData });
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const fields = Array.from(fieldSet).sort();
|
|
373
|
+
if (fields.length === 0) {
|
|
374
|
+
return {
|
|
375
|
+
data: 0,
|
|
376
|
+
sql: { sql: "", params: [], duration: 0 }
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const query = SqlBuilder.toUpdateCaseByIdSql({
|
|
381
|
+
table: snakeTable,
|
|
382
|
+
idField: "id",
|
|
383
|
+
rows: processedList,
|
|
384
|
+
fields: fields,
|
|
385
|
+
quoteIdent: quoteIdentMySql,
|
|
386
|
+
updatedAtField: "updated_at",
|
|
387
|
+
updatedAtValue: now,
|
|
388
|
+
stateField: "state",
|
|
389
|
+
stateGtZero: true
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
const executeRes = await this.execute(query.sql, query.params);
|
|
393
|
+
const changes = toNumberFromSql(executeRes.data?.changes);
|
|
394
|
+
return {
|
|
395
|
+
data: changes,
|
|
396
|
+
sql: executeRes.sql
|
|
397
|
+
};
|
|
398
|
+
},
|
|
399
|
+
|
|
400
|
+
async updData(options) {
|
|
401
|
+
validateTableDataOptions(options, "updData");
|
|
402
|
+
validateTableWhereOptions(options, "updData", true);
|
|
403
|
+
const { table, data, where } = options;
|
|
404
|
+
const snakeTable = snakeCase(table);
|
|
405
|
+
const snakeWhere = whereKeysToSnake(clearDeep(where));
|
|
406
|
+
|
|
407
|
+
const processed = buildUpdateRow({ data: data, now: Date.now(), allowState: true });
|
|
408
|
+
const whereFiltered = addDefaultStateFilter(snakeWhere, snakeTable, false);
|
|
409
|
+
const builder = this.createSqlBuilder().where(whereFiltered);
|
|
410
|
+
const { sql, params } = builder.toUpdateSql(snakeTable, processed);
|
|
411
|
+
|
|
412
|
+
const executeRes = await this.execute(sql, params);
|
|
413
|
+
const changes = toNumberFromSql(executeRes.data?.changes);
|
|
414
|
+
return {
|
|
415
|
+
data: changes,
|
|
416
|
+
sql: executeRes.sql
|
|
417
|
+
};
|
|
418
|
+
},
|
|
419
|
+
|
|
420
|
+
async delData(options) {
|
|
421
|
+
validateTableWhereOptions(options, "delData", true);
|
|
422
|
+
const { table, where } = options;
|
|
423
|
+
|
|
424
|
+
return await this.updData({
|
|
425
|
+
table: table,
|
|
426
|
+
data: { state: 0, deleted_at: Date.now() },
|
|
427
|
+
where: where
|
|
428
|
+
});
|
|
429
|
+
},
|
|
430
|
+
|
|
431
|
+
async delForce(options) {
|
|
432
|
+
validateTableWhereOptions(options, "delForce", true);
|
|
433
|
+
const { table, where } = options;
|
|
434
|
+
|
|
435
|
+
const snakeTable = snakeCase(table);
|
|
436
|
+
const snakeWhere = whereKeysToSnake(clearDeep(where));
|
|
437
|
+
|
|
438
|
+
const builder = this.createSqlBuilder().where(snakeWhere);
|
|
439
|
+
const { sql, params } = builder.toDeleteSql(snakeTable);
|
|
440
|
+
|
|
441
|
+
const executeRes = await this.execute(sql, params);
|
|
442
|
+
const changes = toNumberFromSql(executeRes.data?.changes);
|
|
443
|
+
return {
|
|
444
|
+
data: changes,
|
|
445
|
+
sql: executeRes.sql
|
|
446
|
+
};
|
|
447
|
+
},
|
|
448
|
+
|
|
449
|
+
async disableData(options) {
|
|
450
|
+
validateTableWhereOptions(options, "disableData", true);
|
|
451
|
+
const { table, where } = options;
|
|
452
|
+
|
|
453
|
+
return await this.updData({
|
|
454
|
+
table: table,
|
|
455
|
+
data: {
|
|
456
|
+
state: 2
|
|
457
|
+
},
|
|
458
|
+
where: where
|
|
459
|
+
});
|
|
460
|
+
},
|
|
461
|
+
|
|
462
|
+
async enableData(options) {
|
|
463
|
+
validateTableWhereOptions(options, "enableData", true);
|
|
464
|
+
const { table, where } = options;
|
|
465
|
+
|
|
466
|
+
return await this.updData({
|
|
467
|
+
table: table,
|
|
468
|
+
data: {
|
|
469
|
+
state: 1
|
|
470
|
+
},
|
|
471
|
+
where: where
|
|
472
|
+
});
|
|
473
|
+
},
|
|
474
|
+
|
|
475
|
+
async increment(table, field, where, value = 1) {
|
|
476
|
+
validateIncrementOptions(table, field, where, value, "increment");
|
|
477
|
+
const snakeTable = snakeCase(table);
|
|
478
|
+
const snakeField = snakeCase(field);
|
|
479
|
+
|
|
480
|
+
const snakeWhere = whereKeysToSnake(clearDeep(where));
|
|
481
|
+
const whereFiltered = addDefaultStateFilter(snakeWhere, snakeTable, false);
|
|
482
|
+
const builder = this.createSqlBuilder().where(whereFiltered);
|
|
483
|
+
const { sql: whereClause, params: whereParams } = builder.getWhereConditions();
|
|
484
|
+
|
|
485
|
+
const quotedTable = quoteIdentMySql(snakeTable);
|
|
486
|
+
const quotedField = quoteIdentMySql(snakeField);
|
|
487
|
+
const sql = whereClause ? `UPDATE ${quotedTable} SET ${quotedField} = ${quotedField} + ? WHERE ${whereClause}` : `UPDATE ${quotedTable} SET ${quotedField} = ${quotedField} + ?`;
|
|
488
|
+
|
|
489
|
+
const params = [value];
|
|
490
|
+
for (const param of whereParams) {
|
|
491
|
+
params.push(param);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const executeRes = await this.execute(sql, params);
|
|
495
|
+
const changes = toNumberFromSql(executeRes.data?.changes);
|
|
496
|
+
return {
|
|
497
|
+
data: changes,
|
|
498
|
+
sql: executeRes.sql
|
|
499
|
+
};
|
|
500
|
+
},
|
|
501
|
+
|
|
502
|
+
async decrement(table, field, where, value = 1) {
|
|
503
|
+
return await this.increment(table, field, where, -value);
|
|
504
|
+
}
|
|
505
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { toSqlParams } from "../sqlBuilder/util.js";
|
|
2
|
+
import { snakeCase } from "../../utils/util.js";
|
|
3
|
+
import { DbSqlError } from "./context.js";
|
|
4
|
+
import { validateExecuteSql } from "./validate.js";
|
|
5
|
+
|
|
6
|
+
export const executeMethods = {
|
|
7
|
+
async execute(sql, params) {
|
|
8
|
+
if (!this.sql) {
|
|
9
|
+
throw new Error("数据库连接未初始化", {
|
|
10
|
+
cause: null,
|
|
11
|
+
code: "runtime"
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
validateExecuteSql(sql);
|
|
16
|
+
|
|
17
|
+
const startTime = Date.now();
|
|
18
|
+
const safeParams = toSqlParams(params);
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
let queryResult;
|
|
22
|
+
if (safeParams.length > 0) {
|
|
23
|
+
queryResult = await this.sql.unsafe(sql, safeParams);
|
|
24
|
+
} else {
|
|
25
|
+
queryResult = await this.sql.unsafe(sql);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const duration = Date.now() - startTime;
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
data: queryResult,
|
|
32
|
+
sql: {
|
|
33
|
+
sql: sql,
|
|
34
|
+
params: safeParams,
|
|
35
|
+
duration: duration
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
} catch (error) {
|
|
39
|
+
const duration = Date.now() - startTime;
|
|
40
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
41
|
+
|
|
42
|
+
throw new DbSqlError(`SQL执行失败: ${msg}`, {
|
|
43
|
+
originalError: error,
|
|
44
|
+
params: safeParams,
|
|
45
|
+
duration: duration,
|
|
46
|
+
sqlInfo: {
|
|
47
|
+
sql: sql,
|
|
48
|
+
params: safeParams,
|
|
49
|
+
duration: duration
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
async tableExists(tableName) {
|
|
56
|
+
const snakeTableName = snakeCase(tableName);
|
|
57
|
+
const executeRes = await this.execute("SELECT COUNT(*) as count FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?", [snakeTableName]);
|
|
58
|
+
const exists = (executeRes.data?.[0]?.count || 0) > 0;
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
data: exists,
|
|
62
|
+
sql: executeRes.sql
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
};
|