befly 3.9.39 → 3.10.0
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 +39 -8
- package/befly.config.ts +19 -2
- package/checks/checkApi.ts +79 -77
- package/checks/checkHook.ts +48 -0
- package/checks/checkMenu.ts +168 -0
- package/checks/checkPlugin.ts +48 -0
- package/checks/checkTable.ts +137 -183
- package/docs/README.md +1 -1
- package/docs/api/api.md +1 -1
- package/docs/guide/quickstart.md +16 -9
- package/docs/hooks/hook.md +2 -2
- package/docs/hooks/rateLimit.md +1 -1
- package/docs/infra/redis.md +7 -7
- package/docs/plugins/plugin.md +23 -21
- package/docs/quickstart.md +16 -9
- package/docs/reference/addon.md +12 -1
- package/docs/reference/config.md +13 -30
- package/docs/reference/sync.md +62 -193
- package/docs/reference/table.md +27 -29
- package/hooks/auth.ts +3 -4
- package/hooks/cors.ts +4 -6
- package/hooks/parser.ts +3 -4
- package/hooks/permission.ts +3 -4
- package/hooks/validator.ts +4 -5
- package/lib/cacheHelper.ts +89 -153
- package/lib/cacheKeys.ts +1 -1
- package/lib/connect.ts +9 -13
- package/lib/dbDialect.ts +285 -0
- package/lib/dbHelper.ts +179 -507
- package/lib/dbUtils.ts +450 -0
- package/lib/logger.ts +41 -5
- package/lib/redisHelper.ts +1 -0
- package/lib/sqlBuilder.ts +358 -58
- package/lib/sqlCheck.ts +136 -0
- package/lib/validator.ts +1 -1
- package/loader/loadApis.ts +23 -126
- package/loader/loadHooks.ts +31 -46
- package/loader/loadPlugins.ts +37 -52
- package/main.ts +58 -19
- package/package.json +24 -25
- package/paths.ts +14 -14
- package/plugins/cache.ts +12 -6
- package/plugins/cipher.ts +2 -2
- package/plugins/config.ts +6 -8
- package/plugins/db.ts +14 -19
- package/plugins/jwt.ts +6 -7
- package/plugins/logger.ts +7 -9
- package/plugins/redis.ts +8 -10
- package/plugins/tool.ts +3 -4
- package/router/api.ts +3 -2
- package/router/static.ts +7 -5
- package/sync/syncApi.ts +80 -235
- package/sync/syncCache.ts +16 -0
- package/sync/syncDev.ts +167 -202
- package/sync/syncMenu.ts +230 -444
- package/sync/syncTable.ts +1247 -0
- package/tests/_mocks/mockSqliteDb.ts +204 -0
- package/tests/addonHelper-cache.test.ts +32 -0
- package/tests/apiHandler-routePath-only.test.ts +32 -0
- package/tests/cacheHelper.test.ts +16 -51
- package/tests/checkApi-routePath-strict.test.ts +166 -0
- package/tests/checkMenu.test.ts +346 -0
- package/tests/checkTable-smoke.test.ts +157 -0
- package/tests/dbDialect-cache.test.ts +23 -0
- package/tests/dbDialect.test.ts +46 -0
- package/tests/dbHelper-advanced.test.ts +1 -1
- package/tests/dbHelper-all-array-types.test.ts +15 -15
- package/tests/dbHelper-batch-write.test.ts +90 -0
- package/tests/dbHelper-columns.test.ts +36 -54
- package/tests/dbHelper-execute.test.ts +26 -26
- package/tests/dbHelper-joins.test.ts +85 -176
- package/tests/fixtures/scanFilesAddon/node_modules/@befly-addon/demo/apis/sub/b.ts +3 -0
- package/tests/fixtures/scanFilesApis/a.ts +3 -0
- package/tests/fixtures/scanFilesApis/sub/b.ts +3 -0
- package/tests/loadPlugins-order-smoke.test.ts +75 -0
- package/tests/logger.test.ts +6 -6
- package/tests/redisHelper.test.ts +6 -1
- package/tests/scanFiles-routePath.test.ts +46 -0
- package/tests/smoke-sql.test.ts +24 -0
- package/tests/sqlBuilder-advanced.test.ts +18 -5
- package/tests/sqlBuilder.test.ts +24 -0
- package/tests/sync-init-guard.test.ts +105 -0
- package/tests/syncApi-insBatch-fields-consistent.test.ts +61 -0
- package/tests/syncApi-obsolete-records.test.ts +69 -0
- package/tests/syncApi-type-compat.test.ts +72 -0
- package/tests/syncDev-permissions.test.ts +81 -0
- package/tests/syncMenu-disableMenus-hard-delete.test.ts +88 -0
- package/tests/syncMenu-duplicate-path.test.ts +122 -0
- package/tests/syncMenu-obsolete-records.test.ts +161 -0
- package/tests/syncMenu-parentPath-from-tree.test.ts +75 -0
- package/tests/syncMenu-paths.test.ts +0 -9
- package/tests/{syncDb-apply.test.ts → syncTable-apply.test.ts} +14 -24
- package/tests/{syncDb-array-number.test.ts → syncTable-array-number.test.ts} +31 -31
- package/tests/syncTable-constants.test.ts +101 -0
- package/tests/syncTable-db-integration.test.ts +237 -0
- package/tests/{syncDb-ddl.test.ts → syncTable-ddl.test.ts} +67 -53
- package/tests/{syncDb-helpers.test.ts → syncTable-helpers.test.ts} +12 -26
- package/tests/syncTable-schema.test.ts +99 -0
- package/tests/syncTable-testkit.test.ts +25 -0
- package/tests/syncTable-types.test.ts +122 -0
- package/tests/tableRef-and-deserialize.test.ts +67 -0
- package/tsconfig.json +1 -1
- package/types/api.d.ts +1 -1
- package/types/befly.d.ts +13 -12
- package/types/cache.d.ts +2 -2
- package/types/context.d.ts +1 -1
- package/types/database.d.ts +0 -5
- package/types/hook.d.ts +1 -10
- package/types/plugin.d.ts +2 -96
- package/types/sync.d.ts +19 -25
- package/utils/convertBigIntFields.ts +38 -0
- package/utils/disableMenusGlob.ts +85 -0
- package/utils/importDefault.ts +21 -0
- package/utils/isDirentDirectory.ts +23 -0
- package/utils/loadMenuConfigs.ts +145 -0
- package/utils/processFields.ts +25 -0
- package/utils/scanAddons.ts +72 -0
- package/utils/scanFiles.ts +129 -21
- package/utils/scanSources.ts +64 -0
- package/utils/sortModules.ts +137 -0
- package/checks/checkApp.ts +0 -55
- package/hooks/rateLimit.ts +0 -276
- package/sync/syncAll.ts +0 -35
- package/sync/syncDb/apply.ts +0 -192
- package/sync/syncDb/constants.ts +0 -119
- package/sync/syncDb/ddl.ts +0 -251
- package/sync/syncDb/helpers.ts +0 -84
- package/sync/syncDb/schema.ts +0 -202
- package/sync/syncDb/sqlite.ts +0 -48
- package/sync/syncDb/table.ts +0 -207
- package/sync/syncDb/tableCreate.ts +0 -163
- package/sync/syncDb/types.ts +0 -132
- package/sync/syncDb/version.ts +0 -69
- package/sync/syncDb.ts +0 -168
- package/tests/rateLimit-hook.test.ts +0 -477
- package/tests/syncDb-constants.test.ts +0 -130
- package/tests/syncDb-schema.test.ts +0 -179
- package/tests/syncDb-types.test.ts +0 -139
- package/utils/addonHelper.ts +0 -90
- package/utils/modules.ts +0 -98
- package/utils/route.ts +0 -23
package/lib/connect.ts
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 数据库连接管理器
|
|
3
3
|
* 统一管理 SQL 和 Redis 连接
|
|
4
|
-
* 配置从 beflyConfig 全局对象获取
|
|
5
4
|
*/
|
|
6
5
|
|
|
6
|
+
import type { DatabaseConfig, RedisConfig } from "../types/befly.js";
|
|
7
|
+
|
|
7
8
|
import { SQL, RedisClient } from "bun";
|
|
8
9
|
|
|
9
|
-
import { beflyConfig } from "../befly.config.js";
|
|
10
10
|
import { Logger } from "./logger.js";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* 数据库连接管理器
|
|
14
14
|
* 使用静态方法管理全局单例连接
|
|
15
|
-
* 所有配置从 beflyConfig 自动获取
|
|
16
15
|
*/
|
|
17
16
|
export class Connect {
|
|
18
17
|
private static sqlClient: SQL | null = null;
|
|
@@ -29,11 +28,10 @@ export class Connect {
|
|
|
29
28
|
|
|
30
29
|
/**
|
|
31
30
|
* 连接 SQL 数据库
|
|
32
|
-
* 配置从 beflyConfig.db 获取
|
|
33
31
|
* @returns SQL 客户端实例
|
|
34
32
|
*/
|
|
35
|
-
static async connectSql(): Promise<SQL> {
|
|
36
|
-
const config =
|
|
33
|
+
static async connectSql(dbConfig: DatabaseConfig): Promise<SQL> {
|
|
34
|
+
const config = dbConfig || {};
|
|
37
35
|
|
|
38
36
|
// 构建数据库连接字符串
|
|
39
37
|
const type = config.type || "mysql";
|
|
@@ -137,11 +135,10 @@ export class Connect {
|
|
|
137
135
|
|
|
138
136
|
/**
|
|
139
137
|
* 连接 Redis
|
|
140
|
-
* 配置从 beflyConfig.redis 获取
|
|
141
138
|
* @returns Redis 客户端实例
|
|
142
139
|
*/
|
|
143
|
-
static async connectRedis(): Promise<RedisClient> {
|
|
144
|
-
const config =
|
|
140
|
+
static async connectRedis(redisConfig: RedisConfig): Promise<RedisClient> {
|
|
141
|
+
const config = redisConfig || {};
|
|
145
142
|
|
|
146
143
|
try {
|
|
147
144
|
// 构建 Redis URL
|
|
@@ -212,15 +209,14 @@ export class Connect {
|
|
|
212
209
|
|
|
213
210
|
/**
|
|
214
211
|
* 连接所有数据库(SQL + Redis)
|
|
215
|
-
* 配置从 beflyConfig 自动获取
|
|
216
212
|
*/
|
|
217
|
-
static async connect(): Promise<void> {
|
|
213
|
+
static async connect(config: { db: DatabaseConfig; redis: RedisConfig }): Promise<void> {
|
|
218
214
|
try {
|
|
219
215
|
// 连接 SQL
|
|
220
|
-
await this.connectSql();
|
|
216
|
+
await this.connectSql(config.db || {});
|
|
221
217
|
|
|
222
218
|
// 连接 Redis
|
|
223
|
-
await this.connectRedis();
|
|
219
|
+
await this.connectRedis(config.redis || {});
|
|
224
220
|
} catch (error: any) {
|
|
225
221
|
Logger.error({ err: error }, "数据库初始化失败");
|
|
226
222
|
await this.disconnect();
|
package/lib/dbDialect.ts
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
export type DbDialectName = "mysql" | "postgresql" | "sqlite";
|
|
2
|
+
|
|
3
|
+
let DIALECT_CACHE: Map<DbDialectName, DbDialect> | null = null;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 获取方言实例(内部缓存,避免到处 new)。
|
|
7
|
+
*
|
|
8
|
+
* 约束:dialect 实例应当是纯逻辑对象(无连接/无 IO/无状态副作用),可全局复用。
|
|
9
|
+
*/
|
|
10
|
+
export function getDialectByName(name: DbDialectName): DbDialect {
|
|
11
|
+
const unknownDialectError = new Error(`未知数据库方言: ${String(name)}`);
|
|
12
|
+
if (name !== "mysql" && name !== "postgresql" && name !== "sqlite") {
|
|
13
|
+
throw unknownDialectError;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (!DIALECT_CACHE) {
|
|
17
|
+
DIALECT_CACHE = new Map<DbDialectName, DbDialect>();
|
|
18
|
+
DIALECT_CACHE.set("mysql", new MySqlDialect());
|
|
19
|
+
DIALECT_CACHE.set("postgresql", new PostgresDialect());
|
|
20
|
+
DIALECT_CACHE.set("sqlite", new SqliteDialect());
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const dialect = DIALECT_CACHE.get(name);
|
|
24
|
+
if (!dialect) {
|
|
25
|
+
throw unknownDialectError;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return dialect;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type SqlTextQuery = {
|
|
32
|
+
sql: string;
|
|
33
|
+
params: unknown[];
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type SyncTableColumnsInfoQuery = {
|
|
37
|
+
columns: SqlTextQuery;
|
|
38
|
+
comments?: SqlTextQuery;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type SyncTableIndexesQuery = SqlTextQuery;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* syncTable 专用:获取“列元信息”查询。
|
|
45
|
+
*
|
|
46
|
+
* 说明:
|
|
47
|
+
* - 这里仅负责 SQL + 参数构造(方言差异);
|
|
48
|
+
* - 解析为 ColumnInfo 的逻辑仍放在 syncTable.ts(保持同步算法聚合)。
|
|
49
|
+
*/
|
|
50
|
+
export function getSyncTableColumnsInfoQuery(options: { dialect: DbDialectName; table: string; dbName: string; schema?: string }): SyncTableColumnsInfoQuery {
|
|
51
|
+
if (options.dialect === "mysql") {
|
|
52
|
+
const columns: SqlTextQuery = {
|
|
53
|
+
sql: "SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, IS_NULLABLE, COLUMN_DEFAULT, COLUMN_COMMENT, COLUMN_TYPE FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? ORDER BY ORDINAL_POSITION",
|
|
54
|
+
params: [options.dbName, options.table]
|
|
55
|
+
};
|
|
56
|
+
return { columns: columns };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (options.dialect === "postgresql") {
|
|
60
|
+
const schema = options.schema && options.schema.trim() !== "" ? options.schema : "public";
|
|
61
|
+
const columns: SqlTextQuery = {
|
|
62
|
+
sql: "SELECT column_name, data_type, character_maximum_length, is_nullable, column_default FROM information_schema.columns WHERE table_schema = ? AND table_name = ? ORDER BY ordinal_position",
|
|
63
|
+
params: [schema, options.table]
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const comments: SqlTextQuery = {
|
|
67
|
+
sql: "SELECT a.attname AS column_name, col_description(c.oid, a.attnum) AS column_comment FROM pg_class c JOIN pg_attribute a ON a.attrelid = c.oid JOIN pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind = 'r' AND n.nspname = ? AND c.relname = ? AND a.attnum > 0",
|
|
68
|
+
params: [schema, options.table]
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
return { columns: columns, comments: comments };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const sqlite = getDialectByName("sqlite");
|
|
75
|
+
const columns = sqlite.getTableColumnsQuery(options.table);
|
|
76
|
+
return { columns: columns };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* syncTable 专用:获取“索引元信息”查询(只负责 SQL + 参数构造)。
|
|
81
|
+
*
|
|
82
|
+
* 约束:
|
|
83
|
+
* - 仅下沉 MySQL / PostgreSQL;SQLite 仍走 PRAGMA(需要多次查询)。
|
|
84
|
+
* - 解析(比如 PG indexdef 解析列名)仍留在 syncTable.ts。
|
|
85
|
+
*/
|
|
86
|
+
export function getSyncTableIndexesQuery(options: { dialect: DbDialectName; table: string; dbName: string; schema?: string }): SyncTableIndexesQuery {
|
|
87
|
+
if (options.dialect === "mysql") {
|
|
88
|
+
return {
|
|
89
|
+
sql: "SELECT INDEX_NAME, COLUMN_NAME FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND INDEX_NAME != 'PRIMARY' ORDER BY INDEX_NAME",
|
|
90
|
+
params: [options.dbName, options.table]
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (options.dialect === "postgresql") {
|
|
95
|
+
const schema = options.schema && options.schema.trim() !== "" ? options.schema : "public";
|
|
96
|
+
return {
|
|
97
|
+
sql: "SELECT indexname, indexdef FROM pg_indexes WHERE schemaname = ? AND tablename = ?",
|
|
98
|
+
params: [schema, options.table]
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
throw new Error(`getSyncTableIndexesQuery 不支持方言: ${String(options.dialect)}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface DbDialect {
|
|
106
|
+
name: DbDialectName;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 转义/引用 SQL 标识符(表名/字段名)。
|
|
110
|
+
* 注意:仅用于单个标识符片段,不支持包含空格、点号、函数等复杂表达式。
|
|
111
|
+
*/
|
|
112
|
+
quoteIdent(identifier: string): string;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 获取表字段列表的查询。
|
|
116
|
+
* 约定:返回结果应能通过 getTableColumnsFromResult 提取列名。
|
|
117
|
+
*/
|
|
118
|
+
getTableColumnsQuery(table: string, schema?: string): SqlTextQuery;
|
|
119
|
+
|
|
120
|
+
/** 从 getTableColumnsQuery 的结果中提取列名数组 */
|
|
121
|
+
getTableColumnsFromResult(result: any): string[];
|
|
122
|
+
|
|
123
|
+
/** 检查表是否存在的查询 */
|
|
124
|
+
tableExistsQuery(table: string, schema?: string): SqlTextQuery;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 是否支持 schema.table 形式(仅用于校验;具体 quoting 仍由 SqlBuilder 处理)。
|
|
128
|
+
*/
|
|
129
|
+
supportsSchema: boolean;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export class MySqlDialect implements DbDialect {
|
|
133
|
+
name: DbDialectName = "mysql";
|
|
134
|
+
supportsSchema: boolean = true;
|
|
135
|
+
|
|
136
|
+
quoteIdent(identifier: string): string {
|
|
137
|
+
if (typeof identifier !== "string") {
|
|
138
|
+
throw new Error(`quoteIdent 需要字符串类型标识符 (identifier: ${String(identifier)})`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const trimmed = identifier.trim();
|
|
142
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(trimmed)) {
|
|
143
|
+
throw new Error(`无效的 SQL 标识符: ${trimmed}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return `\`${trimmed}\``;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
getTableColumnsQuery(table: string, _schema?: string): SqlTextQuery {
|
|
150
|
+
const quotedTable = this.quoteIdent(table);
|
|
151
|
+
return { sql: `SHOW COLUMNS FROM ${quotedTable}`, params: [] };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
getTableColumnsFromResult(result: any): string[] {
|
|
155
|
+
if (!Array.isArray(result)) {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const columnNames: string[] = [];
|
|
160
|
+
for (const row of result) {
|
|
161
|
+
const name = row?.Field;
|
|
162
|
+
if (typeof name === "string" && name.length > 0) {
|
|
163
|
+
columnNames.push(name);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return columnNames;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
tableExistsQuery(table: string, schema?: string): SqlTextQuery {
|
|
171
|
+
if (typeof schema === "string" && schema.trim() !== "") {
|
|
172
|
+
return {
|
|
173
|
+
sql: "SELECT COUNT(*) as count FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?",
|
|
174
|
+
params: [schema, table]
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
return { sql: "SELECT COUNT(*) as count FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?", params: [table] };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export class PostgresDialect implements DbDialect {
|
|
182
|
+
name: DbDialectName = "postgresql";
|
|
183
|
+
supportsSchema: boolean = true;
|
|
184
|
+
|
|
185
|
+
quoteIdent(identifier: string): string {
|
|
186
|
+
if (typeof identifier !== "string") {
|
|
187
|
+
throw new Error(`quoteIdent 需要字符串类型标识符 (identifier: ${String(identifier)})`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const trimmed = identifier.trim();
|
|
191
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(trimmed)) {
|
|
192
|
+
throw new Error(`无效的 SQL 标识符: ${trimmed}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// PostgreSQL 使用双引号引用标识符
|
|
196
|
+
return `"${trimmed}"`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
getTableColumnsQuery(table: string, schema?: string): SqlTextQuery {
|
|
200
|
+
if (typeof schema === "string" && schema.trim() !== "") {
|
|
201
|
+
return {
|
|
202
|
+
sql: "SELECT column_name FROM information_schema.columns WHERE table_schema = ? AND table_name = ? ORDER BY ordinal_position",
|
|
203
|
+
params: [schema, table]
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
return { sql: "SELECT column_name FROM information_schema.columns WHERE table_schema = current_schema() AND table_name = ? ORDER BY ordinal_position", params: [table] };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
getTableColumnsFromResult(result: any): string[] {
|
|
210
|
+
if (!Array.isArray(result)) {
|
|
211
|
+
return [];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const columnNames: string[] = [];
|
|
215
|
+
for (const row of result) {
|
|
216
|
+
const name = row?.column_name;
|
|
217
|
+
if (typeof name === "string" && name.length > 0) {
|
|
218
|
+
columnNames.push(name);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return columnNames;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
tableExistsQuery(table: string, schema?: string): SqlTextQuery {
|
|
226
|
+
if (typeof schema === "string" && schema.trim() !== "") {
|
|
227
|
+
return {
|
|
228
|
+
sql: "SELECT COUNT(*)::int as count FROM information_schema.tables WHERE table_schema = ? AND table_name = ?",
|
|
229
|
+
params: [schema, table]
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
return { sql: "SELECT COUNT(*)::int as count FROM information_schema.tables WHERE table_schema = current_schema() AND table_name = ?", params: [table] };
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export class SqliteDialect implements DbDialect {
|
|
237
|
+
name: DbDialectName = "sqlite";
|
|
238
|
+
// SQLite 的“schema”语义与 MySQL/PG 不同:常规使用下没有独立 schema;
|
|
239
|
+
// 虽然存在 main/temp/attached db 的 database_name.table_name 形式,但这里
|
|
240
|
+
// 统一视为不支持传统 schema.table 校验语义。
|
|
241
|
+
supportsSchema: boolean = false;
|
|
242
|
+
|
|
243
|
+
quoteIdent(identifier: string): string {
|
|
244
|
+
if (typeof identifier !== "string") {
|
|
245
|
+
throw new Error(`quoteIdent 需要字符串类型标识符 (identifier: ${String(identifier)})`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const trimmed = identifier.trim();
|
|
249
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(trimmed)) {
|
|
250
|
+
throw new Error(`无效的 SQL 标识符: ${trimmed}`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// SQLite 也支持双引号引用标识符
|
|
254
|
+
return `"${trimmed}"`;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
getTableColumnsQuery(table: string, _schema?: string): SqlTextQuery {
|
|
258
|
+
// PRAGMA 不支持参数占位符;此处通过 quoteIdent 限制输入并安全拼接
|
|
259
|
+
const quotedTable = this.quoteIdent(table);
|
|
260
|
+
return { sql: `PRAGMA table_info(${quotedTable})`, params: [] };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
getTableColumnsFromResult(result: any): string[] {
|
|
264
|
+
if (!Array.isArray(result)) {
|
|
265
|
+
return [];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const columnNames: string[] = [];
|
|
269
|
+
for (const row of result) {
|
|
270
|
+
const name = row?.name;
|
|
271
|
+
if (typeof name === "string" && name.length > 0) {
|
|
272
|
+
columnNames.push(name);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return columnNames;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
tableExistsQuery(table: string, _schema?: string): SqlTextQuery {
|
|
280
|
+
return {
|
|
281
|
+
sql: "SELECT COUNT(*) as count FROM sqlite_master WHERE type = 'table' AND name = ?",
|
|
282
|
+
params: [table]
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
}
|