@carllee1983/dbcli 2.1.0 → 4.0.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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/skills/dbcli/reference.md +38 -6
- package/.cursor-plugin/plugin.json +1 -1
- package/.github/skills/dbcli/reference.md +38 -6
- package/CHANGELOG.md +471 -0
- package/assets/reference.md +38 -6
- package/dist/cli-runtime.mjs +2536 -1332
- package/dist/cli.mjs +4 -1
- package/dist/core.d.ts +172 -420
- package/dist/core.mjs +354 -2697
- package/gemini-extension.json +1 -1
- package/package.json +4 -1
- package/plugins/dbcli-agent/.codex-plugin/plugin.json +1 -1
- package/plugins/dbcli-agent/skills/dbcli/reference.md +38 -6
- package/skills/dbcli/reference.md +38 -6
package/dist/core.mjs
CHANGED
|
@@ -1,35 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var __create = Object.create;
|
|
3
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
4
2
|
var __defProp = Object.defineProperty;
|
|
5
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
-
function __accessProp(key) {
|
|
8
|
-
return this[key];
|
|
9
|
-
}
|
|
10
|
-
var __toESMCache_node;
|
|
11
|
-
var __toESMCache_esm;
|
|
12
|
-
var __toESM = (mod, isNodeMode, target) => {
|
|
13
|
-
var canCache = mod != null && typeof mod === "object";
|
|
14
|
-
if (canCache) {
|
|
15
|
-
var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
|
|
16
|
-
var cached = cache.get(mod);
|
|
17
|
-
if (cached)
|
|
18
|
-
return cached;
|
|
19
|
-
}
|
|
20
|
-
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
21
|
-
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
22
|
-
for (let key of __getOwnPropNames(mod))
|
|
23
|
-
if (!__hasOwnProp.call(to, key))
|
|
24
|
-
__defProp(to, key, {
|
|
25
|
-
get: __accessProp.bind(mod, key),
|
|
26
|
-
enumerable: true
|
|
27
|
-
});
|
|
28
|
-
if (canCache)
|
|
29
|
-
cache.set(mod, to);
|
|
30
|
-
return to;
|
|
31
|
-
};
|
|
32
|
-
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
33
3
|
var __returnValue = (v) => v;
|
|
34
4
|
function __exportSetter(name, newValue) {
|
|
35
5
|
this[name] = __returnValue.bind(null, newValue);
|
|
@@ -46,76 +16,6 @@ var __export = (target, all) => {
|
|
|
46
16
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
47
17
|
var __require = import.meta.require;
|
|
48
18
|
|
|
49
|
-
// node_modules/picocolors/picocolors.js
|
|
50
|
-
var require_picocolors = __commonJS((exports, module) => {
|
|
51
|
-
var p = process || {};
|
|
52
|
-
var argv = p.argv || [];
|
|
53
|
-
var env = p.env || {};
|
|
54
|
-
var isColorSupported = !(!!env.NO_COLOR || argv.includes("--no-color")) && (!!env.FORCE_COLOR || argv.includes("--color") || p.platform === "win32" || (p.stdout || {}).isTTY && env.TERM !== "dumb" || !!env.CI);
|
|
55
|
-
var formatter = (open, close, replace = open) => (input) => {
|
|
56
|
-
let string = "" + input, index = string.indexOf(close, open.length);
|
|
57
|
-
return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close;
|
|
58
|
-
};
|
|
59
|
-
var replaceClose = (string, close, replace, index) => {
|
|
60
|
-
let result = "", cursor = 0;
|
|
61
|
-
do {
|
|
62
|
-
result += string.substring(cursor, index) + replace;
|
|
63
|
-
cursor = index + close.length;
|
|
64
|
-
index = string.indexOf(close, cursor);
|
|
65
|
-
} while (~index);
|
|
66
|
-
return result + string.substring(cursor);
|
|
67
|
-
};
|
|
68
|
-
var createColors = (enabled = isColorSupported) => {
|
|
69
|
-
let f = enabled ? formatter : () => String;
|
|
70
|
-
return {
|
|
71
|
-
isColorSupported: enabled,
|
|
72
|
-
reset: f("\x1B[0m", "\x1B[0m"),
|
|
73
|
-
bold: f("\x1B[1m", "\x1B[22m", "\x1B[22m\x1B[1m"),
|
|
74
|
-
dim: f("\x1B[2m", "\x1B[22m", "\x1B[22m\x1B[2m"),
|
|
75
|
-
italic: f("\x1B[3m", "\x1B[23m"),
|
|
76
|
-
underline: f("\x1B[4m", "\x1B[24m"),
|
|
77
|
-
inverse: f("\x1B[7m", "\x1B[27m"),
|
|
78
|
-
hidden: f("\x1B[8m", "\x1B[28m"),
|
|
79
|
-
strikethrough: f("\x1B[9m", "\x1B[29m"),
|
|
80
|
-
black: f("\x1B[30m", "\x1B[39m"),
|
|
81
|
-
red: f("\x1B[31m", "\x1B[39m"),
|
|
82
|
-
green: f("\x1B[32m", "\x1B[39m"),
|
|
83
|
-
yellow: f("\x1B[33m", "\x1B[39m"),
|
|
84
|
-
blue: f("\x1B[34m", "\x1B[39m"),
|
|
85
|
-
magenta: f("\x1B[35m", "\x1B[39m"),
|
|
86
|
-
cyan: f("\x1B[36m", "\x1B[39m"),
|
|
87
|
-
white: f("\x1B[37m", "\x1B[39m"),
|
|
88
|
-
gray: f("\x1B[90m", "\x1B[39m"),
|
|
89
|
-
bgBlack: f("\x1B[40m", "\x1B[49m"),
|
|
90
|
-
bgRed: f("\x1B[41m", "\x1B[49m"),
|
|
91
|
-
bgGreen: f("\x1B[42m", "\x1B[49m"),
|
|
92
|
-
bgYellow: f("\x1B[43m", "\x1B[49m"),
|
|
93
|
-
bgBlue: f("\x1B[44m", "\x1B[49m"),
|
|
94
|
-
bgMagenta: f("\x1B[45m", "\x1B[49m"),
|
|
95
|
-
bgCyan: f("\x1B[46m", "\x1B[49m"),
|
|
96
|
-
bgWhite: f("\x1B[47m", "\x1B[49m"),
|
|
97
|
-
blackBright: f("\x1B[90m", "\x1B[39m"),
|
|
98
|
-
redBright: f("\x1B[91m", "\x1B[39m"),
|
|
99
|
-
greenBright: f("\x1B[92m", "\x1B[39m"),
|
|
100
|
-
yellowBright: f("\x1B[93m", "\x1B[39m"),
|
|
101
|
-
blueBright: f("\x1B[94m", "\x1B[39m"),
|
|
102
|
-
magentaBright: f("\x1B[95m", "\x1B[39m"),
|
|
103
|
-
cyanBright: f("\x1B[96m", "\x1B[39m"),
|
|
104
|
-
whiteBright: f("\x1B[97m", "\x1B[39m"),
|
|
105
|
-
bgBlackBright: f("\x1B[100m", "\x1B[49m"),
|
|
106
|
-
bgRedBright: f("\x1B[101m", "\x1B[49m"),
|
|
107
|
-
bgGreenBright: f("\x1B[102m", "\x1B[49m"),
|
|
108
|
-
bgYellowBright: f("\x1B[103m", "\x1B[49m"),
|
|
109
|
-
bgBlueBright: f("\x1B[104m", "\x1B[49m"),
|
|
110
|
-
bgMagentaBright: f("\x1B[105m", "\x1B[49m"),
|
|
111
|
-
bgCyanBright: f("\x1B[106m", "\x1B[49m"),
|
|
112
|
-
bgWhiteBright: f("\x1B[107m", "\x1B[49m")
|
|
113
|
-
};
|
|
114
|
-
};
|
|
115
|
-
module.exports = createColors();
|
|
116
|
-
module.exports.createColors = createColors;
|
|
117
|
-
});
|
|
118
|
-
|
|
119
19
|
// node_modules/lru-cache/dist/esm/node/index.min.js
|
|
120
20
|
import { tracingChannel as j, channel as G } from "diagnostics_channel";
|
|
121
21
|
var w, W, C, D = () => w.hasSubscribers || W.hasSubscribers, I, L, P = (u, e, t2, i) => {
|
|
@@ -1245,281 +1145,6 @@ var ENGINE_CAPABILITIES = Object.freeze({
|
|
|
1245
1145
|
shell: cap("limited", "interactive", "Dedicated Kibana Dev Tools-style REPL (method/path + JSON body, blank-line submit); read-focused.")
|
|
1246
1146
|
})
|
|
1247
1147
|
});
|
|
1248
|
-
// src/adapters/error-mapper.ts
|
|
1249
|
-
var TRANSPORT_CODES = {
|
|
1250
|
-
ECONNREFUSED: "ECONNREFUSED",
|
|
1251
|
-
ETIMEDOUT: "ETIMEDOUT",
|
|
1252
|
-
ESOCKETTIMEDOUT: "ETIMEDOUT",
|
|
1253
|
-
ENOTFOUND: "ENOTFOUND",
|
|
1254
|
-
EAI_AGAIN: "ENOTFOUND",
|
|
1255
|
-
PROTOCOL_CONNECTION_LOST: "CONNECTION_LOST",
|
|
1256
|
-
PROTOCOL_SEQUENCE_TIMEOUT: "ETIMEDOUT",
|
|
1257
|
-
ECONNRESET: "CONNECTION_LOST",
|
|
1258
|
-
EPIPE: "CONNECTION_LOST",
|
|
1259
|
-
ECONNABORTED: "CONNECTION_LOST",
|
|
1260
|
-
EHOSTUNREACH: "EHOSTUNREACH",
|
|
1261
|
-
ENETUNREACH: "EHOSTUNREACH",
|
|
1262
|
-
ENETDOWN: "EHOSTUNREACH"
|
|
1263
|
-
};
|
|
1264
|
-
var TLS_CODE_PREFIX = /^(CERT_|SELF_SIGNED_|DEPTH_ZERO_|UNABLE_TO_(?:GET|VERIFY)_|ERR_TLS_|ERR_SSL_)/;
|
|
1265
|
-
var MYSQL_CODES = {
|
|
1266
|
-
ER_NO_SUCH_TABLE: "TABLE_NOT_FOUND",
|
|
1267
|
-
ER_BAD_FIELD_ERROR: "COLUMN_NOT_FOUND",
|
|
1268
|
-
ER_PARSE_ERROR: "SQL_SYNTAX_ERROR",
|
|
1269
|
-
ER_ACCESS_DENIED_ERROR: "AUTH_FAILED",
|
|
1270
|
-
ER_DBACCESS_DENIED_ERROR: "AUTH_FAILED",
|
|
1271
|
-
ER_NOT_SUPPORTED_AUTH_MODE: "AUTH_FAILED",
|
|
1272
|
-
ER_MUST_CHANGE_PASSWORD_LOGIN: "AUTH_FAILED",
|
|
1273
|
-
ER_QUERY_TIMEOUT: "STATEMENT_TIMEOUT",
|
|
1274
|
-
ER_STATEMENT_TIMEOUT: "STATEMENT_TIMEOUT",
|
|
1275
|
-
ER_CON_COUNT_ERROR: "TOO_MANY_CONNECTIONS",
|
|
1276
|
-
ER_SERVER_SHUTDOWN: "CONNECTION_LOST"
|
|
1277
|
-
};
|
|
1278
|
-
var MYSQL_ERRNOS = {
|
|
1279
|
-
1045: "AUTH_FAILED",
|
|
1280
|
-
1044: "AUTH_FAILED",
|
|
1281
|
-
1054: "COLUMN_NOT_FOUND",
|
|
1282
|
-
1064: "SQL_SYNTAX_ERROR",
|
|
1283
|
-
1146: "TABLE_NOT_FOUND",
|
|
1284
|
-
1251: "AUTH_FAILED",
|
|
1285
|
-
1698: "AUTH_FAILED",
|
|
1286
|
-
1040: "TOO_MANY_CONNECTIONS",
|
|
1287
|
-
1053: "CONNECTION_LOST",
|
|
1288
|
-
2006: "CONNECTION_LOST",
|
|
1289
|
-
2013: "CONNECTION_LOST",
|
|
1290
|
-
1969: "STATEMENT_TIMEOUT",
|
|
1291
|
-
3024: "STATEMENT_TIMEOUT"
|
|
1292
|
-
};
|
|
1293
|
-
var SQLSTATES = {
|
|
1294
|
-
"28000": "AUTH_FAILED",
|
|
1295
|
-
"28P01": "AUTH_FAILED",
|
|
1296
|
-
"42601": "SQL_SYNTAX_ERROR",
|
|
1297
|
-
"42P01": "TABLE_NOT_FOUND",
|
|
1298
|
-
"42703": "COLUMN_NOT_FOUND",
|
|
1299
|
-
"57014": "STATEMENT_TIMEOUT",
|
|
1300
|
-
"08000": "CONNECTION_LOST",
|
|
1301
|
-
"08003": "CONNECTION_LOST",
|
|
1302
|
-
"08006": "CONNECTION_LOST",
|
|
1303
|
-
"08001": "ECONNREFUSED",
|
|
1304
|
-
"08004": "CONNECTION_REJECTED",
|
|
1305
|
-
"53300": "TOO_MANY_CONNECTIONS",
|
|
1306
|
-
"57P01": "CONNECTION_LOST",
|
|
1307
|
-
"57P02": "CONNECTION_LOST",
|
|
1308
|
-
"57P03": "SERVER_NOT_READY"
|
|
1309
|
-
};
|
|
1310
|
-
var REDIS_PREFIXES = {
|
|
1311
|
-
NOAUTH: "AUTH_FAILED",
|
|
1312
|
-
WRONGPASS: "AUTH_FAILED"
|
|
1313
|
-
};
|
|
1314
|
-
var FALLBACK_PATTERNS = [
|
|
1315
|
-
[/connection refused/i, "ECONNREFUSED"],
|
|
1316
|
-
[/\bECONNREFUSED\b/, "ECONNREFUSED"],
|
|
1317
|
-
[/connect(?:ion)? time(?:d )?out/i, "ETIMEDOUT"],
|
|
1318
|
-
[/timeout exceeded/i, "ETIMEDOUT"],
|
|
1319
|
-
[/timed out/i, "ETIMEDOUT"],
|
|
1320
|
-
[/connection terminated/i, "CONNECTION_LOST"],
|
|
1321
|
-
[/server closed the connection/i, "CONNECTION_LOST"],
|
|
1322
|
-
[/server has gone away/i, "CONNECTION_LOST"],
|
|
1323
|
-
[/lost connection to \S+ server/i, "CONNECTION_LOST"],
|
|
1324
|
-
[/getaddrinfo/i, "ENOTFOUND"],
|
|
1325
|
-
[/authentication failed/i, "AUTH_FAILED"],
|
|
1326
|
-
[/access denied for user/i, "AUTH_FAILED"],
|
|
1327
|
-
[/no pg_hba\.conf entry/i, "AUTH_FAILED"],
|
|
1328
|
-
[/role\s+".*?"\s+does not exist/i, "AUTH_FAILED"],
|
|
1329
|
-
[/password supplied/i, "AUTH_FAILED"]
|
|
1330
|
-
];
|
|
1331
|
-
var PG_CANCEL_BY_TIMEOUT = /statement timeout/i;
|
|
1332
|
-
function categorize(errCode, errno, errMsg, system) {
|
|
1333
|
-
if (errCode) {
|
|
1334
|
-
if (TLS_CODE_PREFIX.test(errCode))
|
|
1335
|
-
return "TLS_ERROR";
|
|
1336
|
-
const known = TRANSPORT_CODES[errCode] ?? MYSQL_CODES[errCode] ?? SQLSTATES[errCode] ?? REDIS_PREFIXES[errCode];
|
|
1337
|
-
if (known === "STATEMENT_TIMEOUT" && errCode === "57014" && !PG_CANCEL_BY_TIMEOUT.test(errMsg)) {
|
|
1338
|
-
return null;
|
|
1339
|
-
}
|
|
1340
|
-
if (known)
|
|
1341
|
-
return known;
|
|
1342
|
-
}
|
|
1343
|
-
const isMysqlFamily = system === "mysql" || system === "mariadb";
|
|
1344
|
-
if (isMysqlFamily && typeof errno === "number" && MYSQL_ERRNOS[errno]) {
|
|
1345
|
-
return MYSQL_ERRNOS[errno];
|
|
1346
|
-
}
|
|
1347
|
-
const redisPrefix = errMsg.match(/^([A-Z]+)\s/)?.[1];
|
|
1348
|
-
if (redisPrefix && REDIS_PREFIXES[redisPrefix])
|
|
1349
|
-
return REDIS_PREFIXES[redisPrefix];
|
|
1350
|
-
if (errCode || isMysqlFamily && typeof errno === "number")
|
|
1351
|
-
return null;
|
|
1352
|
-
for (const [pattern, category] of FALLBACK_PATTERNS) {
|
|
1353
|
-
if (pattern.test(errMsg))
|
|
1354
|
-
return category;
|
|
1355
|
-
}
|
|
1356
|
-
return null;
|
|
1357
|
-
}
|
|
1358
|
-
var SYSTEM_FACTS = {
|
|
1359
|
-
postgresql: {
|
|
1360
|
-
serviceCheck: "systemctl status postgresql",
|
|
1361
|
-
defaultPort: "default 5432",
|
|
1362
|
-
firewallRule: "allow TCP 5432",
|
|
1363
|
-
client: "psql",
|
|
1364
|
-
logFile: "postgresql.log"
|
|
1365
|
-
},
|
|
1366
|
-
mysql: {
|
|
1367
|
-
serviceCheck: "systemctl status mysql",
|
|
1368
|
-
defaultPort: "default 3306",
|
|
1369
|
-
firewallRule: "allow TCP 3306",
|
|
1370
|
-
client: "mysql",
|
|
1371
|
-
logFile: "mysql.log"
|
|
1372
|
-
},
|
|
1373
|
-
mariadb: {
|
|
1374
|
-
serviceCheck: "systemctl status mariadb",
|
|
1375
|
-
defaultPort: "default 3306",
|
|
1376
|
-
firewallRule: "allow TCP 3306",
|
|
1377
|
-
client: "mariadb",
|
|
1378
|
-
logFile: "mariadb.log"
|
|
1379
|
-
},
|
|
1380
|
-
redis: {
|
|
1381
|
-
serviceCheck: "redis-cli ping",
|
|
1382
|
-
defaultPort: "default 6379",
|
|
1383
|
-
firewallRule: "allow TCP 6379",
|
|
1384
|
-
client: "redis-cli",
|
|
1385
|
-
logFile: "redis.log"
|
|
1386
|
-
}
|
|
1387
|
-
};
|
|
1388
|
-
function serviceHints(system, options) {
|
|
1389
|
-
const facts = SYSTEM_FACTS[system];
|
|
1390
|
-
return [
|
|
1391
|
-
`Check that the ${system} service is running: ${facts.serviceCheck}`,
|
|
1392
|
-
`Verify the port is correct: ${facts.defaultPort}`,
|
|
1393
|
-
`Check that ${options.host} is reachable: ping ${options.host} or telnet ${options.host} ${options.port}`
|
|
1394
|
-
];
|
|
1395
|
-
}
|
|
1396
|
-
function mapError(error, system, options) {
|
|
1397
|
-
if (error instanceof ConnectionError)
|
|
1398
|
-
return error;
|
|
1399
|
-
const err = error;
|
|
1400
|
-
const errMsg = String(err?.message || String(error));
|
|
1401
|
-
const errCode = String(err?.code || "");
|
|
1402
|
-
const errno = err?.errno;
|
|
1403
|
-
switch (categorize(errCode, errno, errMsg, system)) {
|
|
1404
|
-
case "ECONNREFUSED":
|
|
1405
|
-
return new ConnectionError("ECONNREFUSED", `Cannot connect to ${options.host}:${options.port} \u2014 server is not running or not listening on this port`, serviceHints(system, options));
|
|
1406
|
-
case "ETIMEDOUT":
|
|
1407
|
-
return new ConnectionError("ETIMEDOUT", `Connection timed out (${options.timeout || 5000}ms) \u2014 may be blocked by a firewall or slow network`, [
|
|
1408
|
-
`Check firewall rules: ${SYSTEM_FACTS[system].firewallRule}`,
|
|
1409
|
-
`Increase timeout: edit .dbcli and add "timeout": 15000`,
|
|
1410
|
-
`Verify network connectivity: ping ${options.host} -c 3`
|
|
1411
|
-
]);
|
|
1412
|
-
case "CONNECTION_LOST":
|
|
1413
|
-
return new ConnectionError("CONNECTION_LOST", `Connection to ${options.host}:${options.port} was lost mid-session \u2014 the server closed it or the network dropped`, [
|
|
1414
|
-
"Re-run the command: a dropped connection is often transient",
|
|
1415
|
-
`Check whether the server restarted or is cycling: ${SYSTEM_FACTS[system].logFile}`,
|
|
1416
|
-
"If it happens on long-running work, look at the server idle/wait timeout"
|
|
1417
|
-
]);
|
|
1418
|
-
case "SERVER_NOT_READY":
|
|
1419
|
-
return new ConnectionError("SERVER_NOT_READY", `${options.host}:${options.port} is not accepting connections yet \u2014 the server is starting up or recovering`, [
|
|
1420
|
-
"Retry shortly: this clears on its own once startup or recovery finishes",
|
|
1421
|
-
`Watch progress in the server log: ${SYSTEM_FACTS[system].logFile}`,
|
|
1422
|
-
"On a replica, this also appears while it catches up with the primary"
|
|
1423
|
-
]);
|
|
1424
|
-
case "CONNECTION_REJECTED":
|
|
1425
|
-
return new ConnectionError("CONNECTION_REJECTED", `${options.host}:${options.port} answered but rejected the connection attempt`, [
|
|
1426
|
-
"Check the server access rules (pg_hba.conf) for this user, database, and client address",
|
|
1427
|
-
"If a pooler (pgbouncer / ProxySQL) sits in front, check its own rules and limits",
|
|
1428
|
-
"Check whether the connection limit for this user or database is exhausted"
|
|
1429
|
-
]);
|
|
1430
|
-
case "TOO_MANY_CONNECTIONS":
|
|
1431
|
-
return new ConnectionError("TOO_MANY_CONNECTIONS", `${options.host}:${options.port} refused the connection \u2014 the server has no connection slots left`, [
|
|
1432
|
-
"Wait and retry: the limit is on concurrent connections, not on you",
|
|
1433
|
-
system === "postgresql" ? "Inspect usage: SELECT count(*) FROM pg_stat_activity \u2014 compare against max_connections" : 'Inspect usage: SHOW STATUS LIKE "Threads_connected" \u2014 compare against max_connections',
|
|
1434
|
-
"Close idle sessions, or raise max_connections if the load is legitimate"
|
|
1435
|
-
]);
|
|
1436
|
-
case "EHOSTUNREACH":
|
|
1437
|
-
return new ConnectionError("EHOSTUNREACH", `No route to ${options.host} \u2014 the name resolved, but the host or network is unreachable`, [
|
|
1438
|
-
"Check that you are on the right network or VPN",
|
|
1439
|
-
`Check routing and firewall between here and ${options.host}`,
|
|
1440
|
-
`Confirm the address is the one you meant: ${options.host}:${options.port}`
|
|
1441
|
-
]);
|
|
1442
|
-
case "TLS_ERROR":
|
|
1443
|
-
return new ConnectionError("TLS_ERROR", `TLS handshake failed: ${errMsg}`, [
|
|
1444
|
-
'Point the connection at the right CA bundle: set "caPath" in .dbcli',
|
|
1445
|
-
'For a self-signed certificate in a trusted network, set "rejectUnauthorized": false',
|
|
1446
|
-
`Confirm the certificate covers the host you connected to: ${options.host}`
|
|
1447
|
-
]);
|
|
1448
|
-
case "ENOTFOUND":
|
|
1449
|
-
return new ConnectionError("ENOTFOUND", `Host not found: ${options.host}`, [
|
|
1450
|
-
`Check the hostname spelling: ${options.host}`,
|
|
1451
|
-
`Verify DNS resolution: nslookup ${options.host}`,
|
|
1452
|
-
`If using localhost, try 127.0.0.1 (IPv4 vs IPv6 issue)`
|
|
1453
|
-
]);
|
|
1454
|
-
case "AUTH_FAILED":
|
|
1455
|
-
return new ConnectionError("AUTH_FAILED", `Authentication failed \u2014 check your username or password`, [
|
|
1456
|
-
`Verify credentials: ${SYSTEM_FACTS[system].client} ${system === "postgresql" ? "-U" : "-u"} ${options.user} -h ${options.host}`,
|
|
1457
|
-
`Check pg_hba.conf (PostgreSQL) or user privileges (MySQL)`,
|
|
1458
|
-
`Re-run dbcli init to update credentials`
|
|
1459
|
-
]);
|
|
1460
|
-
case "TABLE_NOT_FOUND": {
|
|
1461
|
-
const mysqlMatch = errMsg.match(/Table\s+'(?:[^.']+\.)?([^']+)'/i);
|
|
1462
|
-
const pgMatch = errMsg.match(/relation\s+"([^"]+)"/i);
|
|
1463
|
-
const tableName = mysqlMatch?.[1] || pgMatch?.[1] || "unknown";
|
|
1464
|
-
return new ConnectionError("TABLE_NOT_FOUND", `Table '${tableName}' not found in database '${options.database || "unknown"}'`, [
|
|
1465
|
-
"Run `dbcli list` to see available tables",
|
|
1466
|
-
"Run `dbcli schema <table>` to confirm the exact name (ORM export names may differ from DB table names)"
|
|
1467
|
-
]);
|
|
1468
|
-
}
|
|
1469
|
-
case "COLUMN_NOT_FOUND": {
|
|
1470
|
-
const mysqlMatch = errMsg.match(/Unknown column\s+'([^']+)'/i);
|
|
1471
|
-
const pgMatch = errMsg.match(/column\s+"([^"]+)"\s+does not exist/i);
|
|
1472
|
-
const columnName = mysqlMatch?.[1] || pgMatch?.[1] || "unknown";
|
|
1473
|
-
return new ConnectionError("COLUMN_NOT_FOUND", `Column '${columnName}' not found`, [
|
|
1474
|
-
"Run `dbcli schema <table>` to see the actual column names",
|
|
1475
|
-
"Column names are case-sensitive on some databases (PostgreSQL with quoted identifiers)"
|
|
1476
|
-
]);
|
|
1477
|
-
}
|
|
1478
|
-
case "STATEMENT_TIMEOUT": {
|
|
1479
|
-
const limitMs = options.statementTimeout ?? options.timeout;
|
|
1480
|
-
return new ConnectionError("STATEMENT_TIMEOUT", `Statement timed out${limitMs ? ` (${limitMs}ms)` : ""} \u2014 the server canceled this query before it finished`, [
|
|
1481
|
-
'Inspect the query plan: dbcli explain "<sql>"',
|
|
1482
|
-
"Raise the ceiling for this run: dbcli --statement-timeout <ms> \u2026 (0 removes it)",
|
|
1483
|
-
"Narrow the query: add WHERE filters, reduce the LIMIT, or index the scanned columns"
|
|
1484
|
-
], limitMs);
|
|
1485
|
-
}
|
|
1486
|
-
case "SQL_SYNTAX_ERROR":
|
|
1487
|
-
return new ConnectionError("SQL_SYNTAX_ERROR", `SQL syntax error: ${errMsg}`, [
|
|
1488
|
-
"Check your SQL syntax near the position reported above",
|
|
1489
|
-
"In query-only mode, dbcli auto-appends LIMIT 1000; use --no-limit to disable",
|
|
1490
|
-
"For SHOW/DESCRIBE/EXPLAIN statements, LIMIT is not allowed \u2014 these are not auto-limited"
|
|
1491
|
-
]);
|
|
1492
|
-
}
|
|
1493
|
-
const reportedCode = errCode || (typeof errno === "number" ? String(errno) : "");
|
|
1494
|
-
if (reportedCode) {
|
|
1495
|
-
return new ConnectionError("UNKNOWN", `Database error (${reportedCode}): ${errMsg}`, [
|
|
1496
|
-
"Run `dbcli schema <table>` to confirm the objects referenced by this statement",
|
|
1497
|
-
`Look up ${reportedCode} in the ${system} error reference for the exact cause`
|
|
1498
|
-
]);
|
|
1499
|
-
}
|
|
1500
|
-
return new ConnectionError("UNKNOWN", `Connection failed: ${errMsg}`, [
|
|
1501
|
-
`Check connection parameters: host=${options.host}, port=${options.port}, user=${options.user}`,
|
|
1502
|
-
`View server logs: ${SYSTEM_FACTS[system].logFile}`,
|
|
1503
|
-
`Try connecting directly with the ${SYSTEM_FACTS[system].client} command-line tool`
|
|
1504
|
-
]);
|
|
1505
|
-
}
|
|
1506
|
-
|
|
1507
|
-
// src/adapters/sql-adapter-utils.ts
|
|
1508
|
-
function requireConnected(connection) {
|
|
1509
|
-
if (!connection) {
|
|
1510
|
-
throw new ConnectionError("UNKNOWN", "Database connection not established", [
|
|
1511
|
-
"Call connect() to establish a connection"
|
|
1512
|
-
]);
|
|
1513
|
-
}
|
|
1514
|
-
return connection;
|
|
1515
|
-
}
|
|
1516
|
-
async function withMappedConnectionError(system, options, operation) {
|
|
1517
|
-
try {
|
|
1518
|
-
return await operation();
|
|
1519
|
-
} catch (error) {
|
|
1520
|
-
throw mapError(error, system, options);
|
|
1521
|
-
}
|
|
1522
|
-
}
|
|
1523
1148
|
// resources/lang/en/messages.json
|
|
1524
1149
|
var messages_default = {
|
|
1525
1150
|
init: {
|
|
@@ -1782,7 +1407,8 @@ Reference: {referencePath}`,
|
|
|
1782
1407
|
table_added: "\u2713 Table '{table}' added to blacklist",
|
|
1783
1408
|
table_removed: "\u2713 Table '{table}' removed from blacklist",
|
|
1784
1409
|
column_added: "\u2713 Column '{table}.{column}' added to blacklist",
|
|
1785
|
-
column_removed: "\u2713 Column '{table}.{column}' removed from blacklist"
|
|
1410
|
+
column_removed: "\u2713 Column '{table}.{column}' removed from blacklist",
|
|
1411
|
+
refuse_wildcard: "Refused: '{table}' contains wildcard characters, which are matched literally on {system} \u2014 the entry would never match anything. Write the exact name."
|
|
1786
1412
|
},
|
|
1787
1413
|
security: {
|
|
1788
1414
|
columns_omitted: "Security: {count} column(s) were omitted based on your blacklist"
|
|
@@ -2095,7 +1721,8 @@ var messages_default2 = {
|
|
|
2095
1721
|
table_added: "\u2713 \u8868\u683C '{table}' \u5DF2\u65B0\u589E\u81F3\u9ED1\u540D\u55AE",
|
|
2096
1722
|
table_removed: "\u2713 \u8868\u683C '{table}' \u5DF2\u5F9E\u9ED1\u540D\u55AE\u79FB\u9664",
|
|
2097
1723
|
column_added: "\u2713 \u6B04\u4F4D '{table}.{column}' \u5DF2\u65B0\u589E\u81F3\u9ED1\u540D\u55AE",
|
|
2098
|
-
column_removed: "\u2713 \u6B04\u4F4D '{table}.{column}' \u5DF2\u5F9E\u9ED1\u540D\u55AE\u79FB\u9664"
|
|
1724
|
+
column_removed: "\u2713 \u6B04\u4F4D '{table}.{column}' \u5DF2\u5F9E\u9ED1\u540D\u55AE\u79FB\u9664",
|
|
1725
|
+
refuse_wildcard: "\u5DF2\u62D2\u7D55\uFF1A'{table}' \u542B\u6709\u842C\u7528\u5B57\u5143\uFF0C\u800C {system} \u4E0A\u7684\u6BD4\u5C0D\u662F\u9010\u5B57\u9032\u884C\u7684\u2014\u2014\u9019\u7B46\u8A2D\u5B9A\u6C38\u9060\u4E0D\u6703\u547D\u4E2D\u4EFB\u4F55\u6771\u897F\u3002\u8ACB\u5BEB\u51FA\u78BA\u5207\u7684\u540D\u7A31\u3002"
|
|
2099
1726
|
},
|
|
2100
1727
|
security: {
|
|
2101
1728
|
columns_omitted: "\u5B89\u5168: {count} \u500B\u6B04\u4F4D\u6839\u64DA\u4F60\u7684\u9ED1\u540D\u55AE\u5DF2\u88AB\u96B1\u85CF"
|
|
@@ -2167,7 +1794,27 @@ var shell_default = {
|
|
|
2167
1794
|
timing_display: "Time: {ms}ms",
|
|
2168
1795
|
rows_display: "{count} row(s)",
|
|
2169
1796
|
unknown_command: "Unknown command: '{command}'. Type '.help' for available commands.",
|
|
2170
|
-
subcommand_prefix_hint: "This line is being read as SQL and is waiting for its `;`. If you meant the dbcli subcommand, type {command} instead. Ctrl-C cancels."
|
|
1797
|
+
subcommand_prefix_hint: "This line is being read as SQL and is waiting for its `;`. If you meant the dbcli subcommand, type {command} instead. Ctrl-C cancels.",
|
|
1798
|
+
es: {
|
|
1799
|
+
parse_empty: "Empty request",
|
|
1800
|
+
parse_needs_method_path: 'Request requires a method and a path, e.g. "GET /index/_search"',
|
|
1801
|
+
parse_needs_path: "Request requires a path",
|
|
1802
|
+
refuse_dot_segment: "Refused: '{path}' contains a '..' segment once decoded, and dbcli cannot tell which path Elasticsearch would route it to. Write the path out without '..'.",
|
|
1803
|
+
refuse_unparseable: "Refused: '{path}' is not the request the server would receive. It cannot be parsed as a path.",
|
|
1804
|
+
refuse_not_canonical: "Refused: '{path}' is not the request the server would receive. Write it as '{canonical}'.",
|
|
1805
|
+
refuse_source_param: "Refused: `source` in the query string is a request body by another route. Write the body as JSON.",
|
|
1806
|
+
refuse_search_template: "Refused: '{path}' is a search template, whose body cannot be inspected \u2014 it is a string rendered on the cluster, or stored there. Write the query out in full.",
|
|
1807
|
+
refuse_string_body: "Refused: a quoted string request body is not supported. Write the body as JSON.",
|
|
1808
|
+
refuse_audit_strict: "Refused: the audit entry for this request could not be written, and audit.strict is on. Fix the audit sink (disk space, directory permissions, a stale lockfile in .dbcli/audit) or turn audit.strict off to accept unrecorded requests.",
|
|
1809
|
+
blacklist_index: "index '{index}' is blacklist-protected",
|
|
1810
|
+
blacklist_unscoped: "'{path}' names no index, so it cannot be checked against the blacklist. Scope the request to an index, e.g. GET /<index>/_search.",
|
|
1811
|
+
blacklist_field: "field '{field}' is blacklist-protected and cannot be named in a request \u2014 sorting, aggregating or scripting on it would return its values.",
|
|
1812
|
+
banner_title: "Elasticsearch shell \u2014 Kibana Dev Tools syntax",
|
|
1813
|
+
banner_hint: 'Enter "<METHOD> /<path>" then an optional JSON body; submit with a blank line. Try: GET /_cat/indices',
|
|
1814
|
+
banner_keys: 'Ctrl+C cancels the current block; Ctrl+D or "exit" quits.',
|
|
1815
|
+
block_cancelled: "(block cancelled)",
|
|
1816
|
+
goodbye: "Goodbye"
|
|
1817
|
+
}
|
|
2171
1818
|
};
|
|
2172
1819
|
// resources/lang/zh-TW/shell.json
|
|
2173
1820
|
var shell_default2 = {
|
|
@@ -2190,7 +1837,27 @@ var shell_default2 = {
|
|
|
2190
1837
|
timing_display: "\u8017\u6642\uFF1A{ms}ms",
|
|
2191
1838
|
rows_display: "{count} \u7B46\u8CC7\u6599",
|
|
2192
1839
|
unknown_command: "\u672A\u77E5\u6307\u4EE4\uFF1A'{command}'\u3002\u8F38\u5165 '.help' \u67E5\u770B\u53EF\u7528\u6307\u4EE4\u3002",
|
|
2193
|
-
subcommand_prefix_hint: "\u9019\u4E00\u884C\u88AB\u7576\u6210 SQL\uFF0C\u6B63\u5728\u7B49\u5206\u865F\u3002\u5982\u679C\u4F60\u8981\u7684\u662F dbcli \u5B50\u6307\u4EE4\uFF0C\u8ACB\u6539\u6253 {command}\u3002Ctrl-C \u53EF\u53D6\u6D88\u3002"
|
|
1840
|
+
subcommand_prefix_hint: "\u9019\u4E00\u884C\u88AB\u7576\u6210 SQL\uFF0C\u6B63\u5728\u7B49\u5206\u865F\u3002\u5982\u679C\u4F60\u8981\u7684\u662F dbcli \u5B50\u6307\u4EE4\uFF0C\u8ACB\u6539\u6253 {command}\u3002Ctrl-C \u53EF\u53D6\u6D88\u3002",
|
|
1841
|
+
es: {
|
|
1842
|
+
parse_empty: "\u7A7A\u7684\u8ACB\u6C42",
|
|
1843
|
+
parse_needs_method_path: '\u8ACB\u6C42\u9700\u8981\u4E00\u500B method \u8207\u4E00\u500B path\uFF0C\u4F8B\u5982 "GET /index/_search"',
|
|
1844
|
+
parse_needs_path: "\u8ACB\u6C42\u9700\u8981\u4E00\u500B path",
|
|
1845
|
+
refuse_dot_segment: "\u5DF2\u62D2\u7D55\uFF1A'{path}' \u89E3\u78BC\u5F8C\u542B\u6709 '..' \u5340\u6BB5\uFF0Cdbcli \u7121\u6CD5\u5224\u65B7 Elasticsearch \u6703\u628A\u5B83\u8DEF\u7531\u5230\u54EA\u500B\u8DEF\u5F91\u3002\u8ACB\u628A\u8DEF\u5F91\u5BEB\u6210\u4E0D\u542B '..' \u7684\u5F62\u5F0F\u3002",
|
|
1846
|
+
refuse_unparseable: "\u5DF2\u62D2\u7D55\uFF1A'{path}' \u4E0D\u662F\u4F3A\u670D\u5668\u6703\u6536\u5230\u7684\u90A3\u500B\u8ACB\u6C42\u3002\u5B83\u7121\u6CD5\u88AB\u89E3\u6790\u6210\u4E00\u500B\u8DEF\u5F91\u3002",
|
|
1847
|
+
refuse_not_canonical: "\u5DF2\u62D2\u7D55\uFF1A'{path}' \u4E0D\u662F\u4F3A\u670D\u5668\u6703\u6536\u5230\u7684\u90A3\u500B\u8ACB\u6C42\u3002\u8ACB\u5BEB\u6210 '{canonical}'\u3002",
|
|
1848
|
+
refuse_source_param: "\u5DF2\u62D2\u7D55\uFF1Aquery string \u88E1\u7684 `source` \u662F\u53E6\u4E00\u689D\u8DEF\u5F91\u4E0A\u7684 request body\u3002\u8ACB\u628A body \u5BEB\u6210 JSON\u3002",
|
|
1849
|
+
refuse_search_template: "\u5DF2\u62D2\u7D55\uFF1A'{path}' \u662F search template\uFF0C\u5B83\u7684 body \u7121\u6CD5\u6AA2\u67E5\u2014\u2014\u90A3\u662F\u4E00\u500B\u5728\u53E2\u96C6\u4E0A\u7B97\u51FA\u4F86\u7684\u5B57\u4E32\uFF0C\u6216\u8005\u6839\u672C\u5B58\u5728\u53E2\u96C6\u88E1\u3002\u8ACB\u628A\u67E5\u8A62\u5B8C\u6574\u5BEB\u51FA\u4F86\u3002",
|
|
1850
|
+
refuse_string_body: "\u5DF2\u62D2\u7D55\uFF1A\u4E0D\u652F\u63F4\u4EE5\u5F15\u865F\u5B57\u4E32\u5F62\u5F0F\u5BEB\u7684 request body\u3002\u8ACB\u628A body \u5BEB\u6210 JSON\u3002",
|
|
1851
|
+
refuse_audit_strict: "\u5DF2\u62D2\u7D55\uFF1A\u9019\u500B\u8ACB\u6C42\u7684\u7A3D\u6838\u7D00\u9304\u5BEB\u4E0D\u51FA\u53BB\uFF0C\u800C audit.strict \u662F\u958B\u7684\u3002\u8ACB\u4FEE\u597D\u7A3D\u6838\u8F38\u51FA\u7AEF\uFF08\u78C1\u789F\u7A7A\u9593\u3001\u76EE\u9304\u6B0A\u9650\u3001.dbcli/audit \u88E1\u6B98\u7559\u7684 lockfile\uFF09\uFF0C\u6216\u95DC\u6389 audit.strict \u4EE5\u63A5\u53D7\u6C92\u6709\u7D00\u9304\u7684\u8ACB\u6C42\u3002",
|
|
1852
|
+
blacklist_index: "\u7D22\u5F15 '{index}' \u53D7\u9ED1\u540D\u55AE\u4FDD\u8B77",
|
|
1853
|
+
blacklist_unscoped: "'{path}' \u6307\u4E0D\u51FA\u4EFB\u4F55\u7D22\u5F15\uFF0C\u56E0\u6B64\u7121\u6CD5\u5C0D\u7167\u9ED1\u540D\u55AE\u6AA2\u67E5\u3002\u8ACB\u628A\u8ACB\u6C42\u9650\u5B9A\u5230\u67D0\u500B\u7D22\u5F15\uFF0C\u4F8B\u5982 GET /<index>/_search\u3002",
|
|
1854
|
+
blacklist_field: "\u6B04\u4F4D '{field}' \u53D7\u9ED1\u540D\u55AE\u4FDD\u8B77\uFF0C\u4E0D\u80FD\u51FA\u73FE\u5728\u8ACB\u6C42\u88E1\u2014\u2014\u5C0D\u5B83\u6392\u5E8F\u3001\u805A\u5408\u6216\u8DD1 script \u90FD\u6703\u628A\u5B83\u7684\u503C\u53D6\u56DE\u4F86\u3002",
|
|
1855
|
+
banner_title: "Elasticsearch shell \u2014\u2014 Kibana Dev Tools \u8A9E\u6CD5",
|
|
1856
|
+
banner_hint: '\u8F38\u5165 "<METHOD> /<path>"\uFF0C\u63A5\u8457\u5BEB\u53EF\u9078\u7684 JSON body\uFF1B\u7A7A\u4E00\u884C\u9001\u51FA\u3002\u53EF\u4EE5\u5148\u8A66\uFF1AGET /_cat/indices',
|
|
1857
|
+
banner_keys: 'Ctrl+C \u53D6\u6D88\u76EE\u524D\u9019\u500B block\uFF1BCtrl+D \u6216 "exit" \u96E2\u958B\u3002',
|
|
1858
|
+
block_cancelled: "\uFF08block \u5DF2\u53D6\u6D88\uFF09",
|
|
1859
|
+
goodbye: "\u518D\u898B"
|
|
1860
|
+
}
|
|
2194
1861
|
};
|
|
2195
1862
|
// resources/lang/en/ceremony.json
|
|
2196
1863
|
var ceremony_default = {
|
|
@@ -2344,1105 +2011,22 @@ class MessageLoader {
|
|
|
2344
2011
|
}
|
|
2345
2012
|
return key;
|
|
2346
2013
|
}
|
|
2014
|
+
setLanguage(lang) {
|
|
2015
|
+
this.currentLang = lang;
|
|
2016
|
+
this.messages = {};
|
|
2017
|
+
this.loadMessages();
|
|
2018
|
+
}
|
|
2347
2019
|
interpolate(key, vars) {
|
|
2348
|
-
|
|
2349
|
-
for (const [varName, value] of Object.entries(vars)) {
|
|
2350
|
-
const escapedVarName = varName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2351
|
-
const regex = new RegExp(`{${escapedVarName}}`, "g");
|
|
2352
|
-
message = message.replace(regex, String(value));
|
|
2353
|
-
}
|
|
2354
|
-
return message;
|
|
2020
|
+
return this.t(key).replace(/\{([^{}]+)\}/g, (match, name) => Object.prototype.hasOwnProperty.call(vars, name) ? String(vars[name]) : match);
|
|
2355
2021
|
}
|
|
2356
2022
|
}
|
|
2357
2023
|
var messageLoader = MessageLoader.getInstance();
|
|
2358
2024
|
var t = (key) => messageLoader.t(key);
|
|
2359
2025
|
var t_vars = (key, vars) => messageLoader.interpolate(key, vars);
|
|
2360
2026
|
|
|
2361
|
-
// src/
|
|
2362
|
-
var
|
|
2363
|
-
var
|
|
2364
|
-
success: (text) => import_picocolors.default.green(text),
|
|
2365
|
-
error: (text) => import_picocolors.default.red(text),
|
|
2366
|
-
warn: (text) => import_picocolors.default.yellow(text),
|
|
2367
|
-
info: (text) => import_picocolors.default.blue(text),
|
|
2368
|
-
dim: (text) => import_picocolors.default.dim(text),
|
|
2369
|
-
bold: (text) => import_picocolors.default.bold(text),
|
|
2370
|
-
keyword: (text) => import_picocolors.default.blue(import_picocolors.default.bold(text))
|
|
2371
|
-
};
|
|
2372
|
-
|
|
2373
|
-
// src/utils/db-version-check.ts
|
|
2374
|
-
var MIN_SUPPORTED_VERSIONS = {
|
|
2375
|
-
postgresql: "12.0",
|
|
2376
|
-
mysql: "8.0",
|
|
2377
|
-
mariadb: "10.5",
|
|
2378
|
-
elasticsearch: "7.10"
|
|
2379
|
-
};
|
|
2380
|
-
function parseVersionSegments(version) {
|
|
2381
|
-
const match = version.match(/^(\d+(?:\.\d+)*)/);
|
|
2382
|
-
if (!match)
|
|
2383
|
-
return [];
|
|
2384
|
-
return (match[1] ?? "").split(".").map(Number);
|
|
2385
|
-
}
|
|
2386
|
-
function isMariaDBVersion(versionString) {
|
|
2387
|
-
return /mariadb/i.test(versionString);
|
|
2388
|
-
}
|
|
2389
|
-
function extractMariaDBVersion(versionString) {
|
|
2390
|
-
const match = versionString.match(/(\d+\.\d+\.\d+)-MariaDB/i);
|
|
2391
|
-
if (match)
|
|
2392
|
-
return match[1] ?? versionString;
|
|
2393
|
-
const prefixMatch = versionString.match(/^5\.5\.5-(\d+\.\d+\.\d+)/);
|
|
2394
|
-
if (prefixMatch)
|
|
2395
|
-
return prefixMatch[1] ?? versionString;
|
|
2396
|
-
return versionString;
|
|
2397
|
-
}
|
|
2398
|
-
function compareVersions(a, b) {
|
|
2399
|
-
const segA = parseVersionSegments(a);
|
|
2400
|
-
const segB = parseVersionSegments(b);
|
|
2401
|
-
const len = Math.max(segA.length, segB.length);
|
|
2402
|
-
for (let i = 0;i < len; i++) {
|
|
2403
|
-
const diff = (segA[i] ?? 0) - (segB[i] ?? 0);
|
|
2404
|
-
if (diff !== 0)
|
|
2405
|
-
return diff;
|
|
2406
|
-
}
|
|
2407
|
-
return 0;
|
|
2408
|
-
}
|
|
2409
|
-
function checkDbVersion(rawVersion, declaredSystem) {
|
|
2410
|
-
const isMariaDB = isMariaDBVersion(rawVersion);
|
|
2411
|
-
const system = isMariaDB ? "mariadb" : declaredSystem;
|
|
2412
|
-
const serverVersion = isMariaDB ? extractMariaDBVersion(rawVersion) : rawVersion;
|
|
2413
|
-
const minVersion = MIN_SUPPORTED_VERSIONS[system];
|
|
2414
|
-
const supported = compareVersions(serverVersion, minVersion ?? "0") >= 0;
|
|
2415
|
-
return {
|
|
2416
|
-
serverVersion,
|
|
2417
|
-
system,
|
|
2418
|
-
supported,
|
|
2419
|
-
minVersion: minVersion ?? "0"
|
|
2420
|
-
};
|
|
2421
|
-
}
|
|
2422
|
-
function warnIfUnsupported(result) {
|
|
2423
|
-
if (result.supported)
|
|
2424
|
-
return;
|
|
2425
|
-
const message = t_vars("version.unsupported_warning", {
|
|
2426
|
-
system: result.system,
|
|
2427
|
-
version: result.serverVersion,
|
|
2428
|
-
minVersion: result.minVersion
|
|
2429
|
-
});
|
|
2430
|
-
process.stderr.write(colors.warn(`\u26A0 ${message}`) + `
|
|
2431
|
-
`);
|
|
2432
|
-
}
|
|
2433
|
-
|
|
2434
|
-
// src/utils/encoding.ts
|
|
2435
|
-
var CP1252_REVERSE = {
|
|
2436
|
-
8364: 128,
|
|
2437
|
-
8218: 130,
|
|
2438
|
-
402: 131,
|
|
2439
|
-
8222: 132,
|
|
2440
|
-
8230: 133,
|
|
2441
|
-
8224: 134,
|
|
2442
|
-
8225: 135,
|
|
2443
|
-
710: 136,
|
|
2444
|
-
8240: 137,
|
|
2445
|
-
352: 138,
|
|
2446
|
-
8249: 139,
|
|
2447
|
-
338: 140,
|
|
2448
|
-
381: 142,
|
|
2449
|
-
8216: 145,
|
|
2450
|
-
8217: 146,
|
|
2451
|
-
8220: 147,
|
|
2452
|
-
8221: 148,
|
|
2453
|
-
8226: 149,
|
|
2454
|
-
8211: 150,
|
|
2455
|
-
8212: 151,
|
|
2456
|
-
732: 152,
|
|
2457
|
-
8482: 153,
|
|
2458
|
-
353: 154,
|
|
2459
|
-
8250: 155,
|
|
2460
|
-
339: 156,
|
|
2461
|
-
382: 158,
|
|
2462
|
-
376: 159
|
|
2463
|
-
};
|
|
2464
|
-
function fixDoubleEncodedUtf8(str) {
|
|
2465
|
-
if (!str)
|
|
2466
|
-
return str;
|
|
2467
|
-
const bytes = [];
|
|
2468
|
-
for (const char of str) {
|
|
2469
|
-
const code = char.codePointAt(0);
|
|
2470
|
-
if (code < 128) {
|
|
2471
|
-
bytes.push(code);
|
|
2472
|
-
} else if (code <= 255) {
|
|
2473
|
-
bytes.push(code);
|
|
2474
|
-
} else {
|
|
2475
|
-
const cp1252Byte = CP1252_REVERSE[code];
|
|
2476
|
-
if (cp1252Byte !== undefined) {
|
|
2477
|
-
bytes.push(cp1252Byte);
|
|
2478
|
-
} else {
|
|
2479
|
-
return str;
|
|
2480
|
-
}
|
|
2481
|
-
}
|
|
2482
|
-
}
|
|
2483
|
-
try {
|
|
2484
|
-
const decoded = new TextDecoder("utf-8", { fatal: true }).decode(new Uint8Array(bytes));
|
|
2485
|
-
if (decoded !== str) {
|
|
2486
|
-
return decoded;
|
|
2487
|
-
}
|
|
2488
|
-
} catch {}
|
|
2489
|
-
return str;
|
|
2490
|
-
}
|
|
2491
|
-
|
|
2492
|
-
// src/adapters/identifier-quote.ts
|
|
2493
|
-
function quoteCharFor(dialect) {
|
|
2494
|
-
return dialect === "postgresql" ? '"' : "`";
|
|
2495
|
-
}
|
|
2496
|
-
function assertQuotable(name, what) {
|
|
2497
|
-
if (name.length === 0) {
|
|
2498
|
-
throw new Error(`${what} must not be empty`);
|
|
2499
|
-
}
|
|
2500
|
-
if (name.includes(String.fromCharCode(0))) {
|
|
2501
|
-
throw new Error(`${what} must not contain a NUL character`);
|
|
2502
|
-
}
|
|
2503
|
-
}
|
|
2504
|
-
function quoteIdentifier(name, dialect) {
|
|
2505
|
-
assertQuotable(name, "identifier");
|
|
2506
|
-
const q = quoteCharFor(dialect);
|
|
2507
|
-
return `${q}${name.split(q).join(q + q)}${q}`;
|
|
2508
|
-
}
|
|
2509
|
-
function encodeEsPathSegment(value) {
|
|
2510
|
-
if (value.length === 0) {
|
|
2511
|
-
throw new Error("Elasticsearch path segment must not be empty");
|
|
2512
|
-
}
|
|
2513
|
-
if (value === "." || value === "..") {
|
|
2514
|
-
return value.replaceAll(".", "%2E");
|
|
2515
|
-
}
|
|
2516
|
-
return encodeURIComponent(value);
|
|
2517
|
-
}
|
|
2518
|
-
function encodeEsIndexExpression(expression) {
|
|
2519
|
-
if (expression.length === 0) {
|
|
2520
|
-
throw new Error("Elasticsearch index expression must not be empty");
|
|
2521
|
-
}
|
|
2522
|
-
return expression.split(",").map((part) => encodeEsPathSegment(part)).join(",");
|
|
2523
|
-
}
|
|
2524
|
-
|
|
2525
|
-
// src/adapters/timeout-policy.ts
|
|
2526
|
-
var DEFAULT_CONNECT_TIMEOUT_MS = 5000;
|
|
2527
|
-
function resolveTimeoutPolicy(options) {
|
|
2528
|
-
return {
|
|
2529
|
-
connectMs: options.timeout ?? DEFAULT_CONNECT_TIMEOUT_MS,
|
|
2530
|
-
statementMs: options.statementTimeout ?? options.timeout
|
|
2531
|
-
};
|
|
2532
|
-
}
|
|
2533
|
-
function statementTimeoutSql(system, ms) {
|
|
2534
|
-
if (system === "mariadb") {
|
|
2535
|
-
return `SET SESSION max_statement_time = ${ms / 1000}`;
|
|
2536
|
-
}
|
|
2537
|
-
return `SET SESSION max_execution_time = ${Math.round(ms)}`;
|
|
2538
|
-
}
|
|
2539
|
-
|
|
2540
|
-
// src/adapters/postgresql-adapter.ts
|
|
2541
|
-
class PostgreSQLAdapter {
|
|
2542
|
-
pool = null;
|
|
2543
|
-
client = null;
|
|
2544
|
-
options;
|
|
2545
|
-
constructor(options) {
|
|
2546
|
-
this.options = options;
|
|
2547
|
-
if (options.port < 1 || options.port > 65535) {
|
|
2548
|
-
throw new Error(`Invalid port number: ${options.port}`);
|
|
2549
|
-
}
|
|
2550
|
-
}
|
|
2551
|
-
async connect() {
|
|
2552
|
-
await withMappedConnectionError("postgresql", this.options, async () => {
|
|
2553
|
-
const timeouts = resolveTimeoutPolicy(this.options);
|
|
2554
|
-
const { Pool } = await import("pg");
|
|
2555
|
-
this.pool = new Pool({
|
|
2556
|
-
host: this.options.host,
|
|
2557
|
-
port: this.options.port,
|
|
2558
|
-
user: this.options.user,
|
|
2559
|
-
password: this.options.password,
|
|
2560
|
-
database: this.options.database,
|
|
2561
|
-
connectionTimeoutMillis: timeouts.connectMs,
|
|
2562
|
-
...timeouts.statementMs !== undefined && { statement_timeout: timeouts.statementMs }
|
|
2563
|
-
});
|
|
2564
|
-
await this.testConnection();
|
|
2565
|
-
try {
|
|
2566
|
-
const rawVersion = await this.getServerVersion();
|
|
2567
|
-
const result = checkDbVersion(rawVersion, "postgresql");
|
|
2568
|
-
warnIfUnsupported(result);
|
|
2569
|
-
} catch {}
|
|
2570
|
-
});
|
|
2571
|
-
}
|
|
2572
|
-
async disconnect() {
|
|
2573
|
-
try {
|
|
2574
|
-
if (this.client) {
|
|
2575
|
-
await this.client.release();
|
|
2576
|
-
this.client = null;
|
|
2577
|
-
}
|
|
2578
|
-
if (this.pool) {
|
|
2579
|
-
await this.pool.end();
|
|
2580
|
-
this.pool = null;
|
|
2581
|
-
}
|
|
2582
|
-
} catch {}
|
|
2583
|
-
}
|
|
2584
|
-
async testConnection() {
|
|
2585
|
-
requireConnected(this.pool);
|
|
2586
|
-
return withMappedConnectionError("postgresql", this.options, async () => {
|
|
2587
|
-
const result = await this.execute("SELECT 1 as count");
|
|
2588
|
-
return result.rows.length > 0;
|
|
2589
|
-
});
|
|
2590
|
-
}
|
|
2591
|
-
async execute(sql, params) {
|
|
2592
|
-
const pool = requireConnected(this.pool);
|
|
2593
|
-
return withMappedConnectionError("postgresql", this.options, async () => {
|
|
2594
|
-
const result = params ? await pool.query(sql, params) : await pool.query(sql);
|
|
2595
|
-
return {
|
|
2596
|
-
rows: result.rows,
|
|
2597
|
-
affectedRows: result.rowCount ?? 0
|
|
2598
|
-
};
|
|
2599
|
-
});
|
|
2600
|
-
}
|
|
2601
|
-
async getServerVersion() {
|
|
2602
|
-
const result = await this.execute("SHOW server_version");
|
|
2603
|
-
return result.rows[0]?.server_version ?? "unknown";
|
|
2604
|
-
}
|
|
2605
|
-
async listTables() {
|
|
2606
|
-
requireConnected(this.pool);
|
|
2607
|
-
return withMappedConnectionError("postgresql", this.options, async () => {
|
|
2608
|
-
const query = `
|
|
2609
|
-
SELECT
|
|
2610
|
-
n.nspname as schema_name,
|
|
2611
|
-
c.relname as table_name,
|
|
2612
|
-
c.reltuples::bigint as estimated_rows,
|
|
2613
|
-
CASE c.relkind
|
|
2614
|
-
WHEN 'r' THEN 'table'
|
|
2615
|
-
WHEN 'v' THEN 'view'
|
|
2616
|
-
WHEN 'm' THEN 'view'
|
|
2617
|
-
ELSE 'table'
|
|
2618
|
-
END as table_type,
|
|
2619
|
-
(SELECT COUNT(*) FROM information_schema.columns ic
|
|
2620
|
-
WHERE ic.table_schema = 'public' AND ic.table_name = c.relname)::int as column_count
|
|
2621
|
-
FROM pg_class c
|
|
2622
|
-
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
2623
|
-
WHERE n.nspname = 'public'
|
|
2624
|
-
AND c.relkind IN ('r', 'v', 'm')
|
|
2625
|
-
ORDER BY c.relname
|
|
2626
|
-
`;
|
|
2627
|
-
const result = await this.execute(query);
|
|
2628
|
-
return result.rows.map((row) => ({
|
|
2629
|
-
name: row.table_name,
|
|
2630
|
-
schema: row.schema_name,
|
|
2631
|
-
columns: [],
|
|
2632
|
-
columnCount: row.column_count,
|
|
2633
|
-
rowCount: Math.max(0, row.estimated_rows || 0),
|
|
2634
|
-
engine: "PostgreSQL",
|
|
2635
|
-
estimatedRowCount: Math.max(0, row.estimated_rows || 0),
|
|
2636
|
-
tableType: row.table_type
|
|
2637
|
-
}));
|
|
2638
|
-
});
|
|
2639
|
-
}
|
|
2640
|
-
async getTableSchema(tableName, options) {
|
|
2641
|
-
requireConnected(this.pool);
|
|
2642
|
-
return withMappedConnectionError("postgresql", this.options, async () => {
|
|
2643
|
-
const columnQuery = `
|
|
2644
|
-
SELECT
|
|
2645
|
-
c.column_name as name,
|
|
2646
|
-
c.data_type as type,
|
|
2647
|
-
c.is_nullable = 'YES' as nullable,
|
|
2648
|
-
c.column_default as default_value,
|
|
2649
|
-
(
|
|
2650
|
-
SELECT EXISTS(
|
|
2651
|
-
SELECT 1 FROM information_schema.table_constraints tc
|
|
2652
|
-
JOIN information_schema.key_column_usage kcu
|
|
2653
|
-
ON tc.constraint_catalog = kcu.constraint_catalog
|
|
2654
|
-
AND tc.constraint_schema = kcu.constraint_schema
|
|
2655
|
-
AND tc.constraint_name = kcu.constraint_name
|
|
2656
|
-
WHERE tc.table_catalog = c.table_catalog
|
|
2657
|
-
AND tc.table_schema = c.table_schema
|
|
2658
|
-
AND tc.table_name = c.table_name
|
|
2659
|
-
AND kcu.table_catalog = c.table_catalog
|
|
2660
|
-
AND kcu.table_schema = c.table_schema
|
|
2661
|
-
AND kcu.table_name = c.table_name
|
|
2662
|
-
AND tc.constraint_type = 'PRIMARY KEY'
|
|
2663
|
-
AND kcu.column_name = c.column_name
|
|
2664
|
-
)
|
|
2665
|
-
) as is_primary_key,
|
|
2666
|
-
(c.column_default LIKE 'nextval%') as auto_increment,
|
|
2667
|
-
pgd.description as comment
|
|
2668
|
-
FROM information_schema.columns c
|
|
2669
|
-
LEFT JOIN pg_catalog.pg_statio_all_tables st
|
|
2670
|
-
ON st.schemaname = c.table_schema AND st.relname = c.table_name
|
|
2671
|
-
LEFT JOIN pg_catalog.pg_description pgd
|
|
2672
|
-
ON pgd.objoid = st.relid AND pgd.objsubid = c.ordinal_position
|
|
2673
|
-
WHERE c.table_name = $1
|
|
2674
|
-
AND c.table_schema = 'public'
|
|
2675
|
-
ORDER BY c.ordinal_position
|
|
2676
|
-
`;
|
|
2677
|
-
const columnResult = await this.execute(columnQuery, [tableName]);
|
|
2678
|
-
const columns = columnResult.rows;
|
|
2679
|
-
const enumQuery = `
|
|
2680
|
-
SELECT
|
|
2681
|
-
c.column_name as name,
|
|
2682
|
-
array_agg(e.enumlabel ORDER BY e.enumsortorder) as enum_values
|
|
2683
|
-
FROM information_schema.columns c
|
|
2684
|
-
JOIN pg_catalog.pg_type AS enum_type
|
|
2685
|
-
ON enum_type.typname = c.udt_name
|
|
2686
|
-
JOIN pg_catalog.pg_namespace AS enum_schema
|
|
2687
|
-
ON enum_schema.oid = enum_type.typnamespace
|
|
2688
|
-
AND enum_schema.nspname = c.udt_schema
|
|
2689
|
-
JOIN pg_catalog.pg_enum AS e ON e.enumtypid = enum_type.oid
|
|
2690
|
-
WHERE c.table_name = $1
|
|
2691
|
-
AND c.table_schema = 'public'
|
|
2692
|
-
GROUP BY c.column_name
|
|
2693
|
-
`;
|
|
2694
|
-
const enumResult = await this.execute(enumQuery, [tableName]);
|
|
2695
|
-
const enumResults = enumResult.rows;
|
|
2696
|
-
const enumMap = new Map;
|
|
2697
|
-
for (const row of enumResults) {
|
|
2698
|
-
enumMap.set(row.name, Array.isArray(row.enum_values) ? row.enum_values : []);
|
|
2699
|
-
}
|
|
2700
|
-
const fkQuery = `
|
|
2701
|
-
SELECT
|
|
2702
|
-
constraint_info.conname as name,
|
|
2703
|
-
array_agg(source_column.attname ORDER BY source_key.ordinality) as columns,
|
|
2704
|
-
referenced_schema.nspname as ref_schema,
|
|
2705
|
-
referenced_table.relname as ref_table,
|
|
2706
|
-
array_agg(referenced_column.attname ORDER BY source_key.ordinality) as ref_columns
|
|
2707
|
-
FROM pg_catalog.pg_constraint AS constraint_info
|
|
2708
|
-
JOIN pg_catalog.pg_class AS source_table
|
|
2709
|
-
ON source_table.oid = constraint_info.conrelid
|
|
2710
|
-
JOIN pg_catalog.pg_namespace AS source_schema
|
|
2711
|
-
ON source_schema.oid = source_table.relnamespace
|
|
2712
|
-
JOIN pg_catalog.pg_class AS referenced_table
|
|
2713
|
-
ON referenced_table.oid = constraint_info.confrelid
|
|
2714
|
-
JOIN pg_catalog.pg_namespace AS referenced_schema
|
|
2715
|
-
ON referenced_schema.oid = referenced_table.relnamespace
|
|
2716
|
-
JOIN LATERAL unnest(constraint_info.conkey) WITH ORDINALITY
|
|
2717
|
-
AS source_key(attnum, ordinality) ON TRUE
|
|
2718
|
-
JOIN LATERAL unnest(constraint_info.confkey) WITH ORDINALITY
|
|
2719
|
-
AS referenced_key(attnum, ordinality)
|
|
2720
|
-
ON referenced_key.ordinality = source_key.ordinality
|
|
2721
|
-
JOIN pg_catalog.pg_attribute AS source_column
|
|
2722
|
-
ON source_column.attrelid = constraint_info.conrelid
|
|
2723
|
-
AND source_column.attnum = source_key.attnum
|
|
2724
|
-
JOIN pg_catalog.pg_attribute AS referenced_column
|
|
2725
|
-
ON referenced_column.attrelid = constraint_info.confrelid
|
|
2726
|
-
AND referenced_column.attnum = referenced_key.attnum
|
|
2727
|
-
WHERE constraint_info.contype = 'f'
|
|
2728
|
-
AND source_table.relname = $1
|
|
2729
|
-
AND source_schema.nspname = 'public'
|
|
2730
|
-
GROUP BY
|
|
2731
|
-
constraint_info.oid,
|
|
2732
|
-
constraint_info.conname,
|
|
2733
|
-
referenced_schema.nspname,
|
|
2734
|
-
referenced_table.relname
|
|
2735
|
-
ORDER BY constraint_info.oid
|
|
2736
|
-
`;
|
|
2737
|
-
const fkResult = await this.execute(fkQuery, [tableName]);
|
|
2738
|
-
const fkResults = fkResult.rows;
|
|
2739
|
-
const fkMap = new Map;
|
|
2740
|
-
for (const fk of fkResults) {
|
|
2741
|
-
if (fk.columns.length === 1 && fk.ref_columns.length === 1) {
|
|
2742
|
-
fkMap.set(fk.columns[0], { table: fk.ref_table, column: fk.ref_columns[0] });
|
|
2743
|
-
}
|
|
2744
|
-
}
|
|
2745
|
-
const indexQuery = `
|
|
2746
|
-
SELECT
|
|
2747
|
-
i.relname as name,
|
|
2748
|
-
array_agg(a.attname ORDER BY array_position(ix.indkey, a.attnum)) as columns,
|
|
2749
|
-
ix.indisunique as is_unique
|
|
2750
|
-
FROM pg_index ix
|
|
2751
|
-
JOIN pg_class t ON t.oid = ix.indrelid
|
|
2752
|
-
JOIN pg_class i ON i.oid = ix.indexrelid
|
|
2753
|
-
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
2754
|
-
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey)
|
|
2755
|
-
WHERE t.relname = $1
|
|
2756
|
-
AND n.nspname = 'public'
|
|
2757
|
-
AND NOT ix.indisprimary
|
|
2758
|
-
GROUP BY i.relname, ix.indisunique
|
|
2759
|
-
`;
|
|
2760
|
-
const indexResult = await this.execute(indexQuery, [tableName]);
|
|
2761
|
-
const indexResults = indexResult.rows;
|
|
2762
|
-
const estimateQuery = `
|
|
2763
|
-
SELECT source_table.reltuples::bigint as estimated_rows
|
|
2764
|
-
FROM pg_catalog.pg_class AS source_table
|
|
2765
|
-
JOIN pg_catalog.pg_namespace AS source_schema
|
|
2766
|
-
ON source_schema.oid = source_table.relnamespace
|
|
2767
|
-
WHERE source_table.relname = $1
|
|
2768
|
-
AND source_schema.nspname = 'public'
|
|
2769
|
-
`;
|
|
2770
|
-
const estimateResult = await this.execute(estimateQuery, [
|
|
2771
|
-
tableName
|
|
2772
|
-
]);
|
|
2773
|
-
const estimateResults = estimateResult.rows;
|
|
2774
|
-
const pkQuery = `
|
|
2775
|
-
SELECT
|
|
2776
|
-
array_agg(primary_key_column.attname ORDER BY primary_key.ordinality) as columns
|
|
2777
|
-
FROM pg_catalog.pg_index AS index_info
|
|
2778
|
-
JOIN pg_catalog.pg_class AS source_table
|
|
2779
|
-
ON source_table.oid = index_info.indrelid
|
|
2780
|
-
JOIN pg_catalog.pg_namespace AS source_schema
|
|
2781
|
-
ON source_schema.oid = source_table.relnamespace
|
|
2782
|
-
JOIN LATERAL unnest(index_info.indkey) WITH ORDINALITY
|
|
2783
|
-
AS primary_key(attnum, ordinality) ON TRUE
|
|
2784
|
-
JOIN pg_catalog.pg_attribute AS primary_key_column
|
|
2785
|
-
ON primary_key_column.attrelid = source_table.oid
|
|
2786
|
-
AND primary_key_column.attnum = primary_key.attnum
|
|
2787
|
-
WHERE index_info.indisprimary
|
|
2788
|
-
AND source_table.relname = $1
|
|
2789
|
-
AND source_schema.nspname = 'public'
|
|
2790
|
-
`;
|
|
2791
|
-
const pkResult = await this.execute(pkQuery, [tableName]);
|
|
2792
|
-
const pkResults = pkResult.rows;
|
|
2793
|
-
const qualifiedTableName = `${quoteIdentifier("public", "postgresql")}.${quoteIdentifier(tableName, "postgresql")}`;
|
|
2794
|
-
const exactRowCount = options?.exactRowCount === false ? undefined : (await this.execute(`SELECT COUNT(*) as count FROM ${qualifiedTableName}`)).rows[0]?.count;
|
|
2795
|
-
const primaryKeyArray = Array.isArray(pkResults[0]?.columns) ? pkResults[0].columns : [];
|
|
2796
|
-
const safeForeignKeys = fkResults.map((fk) => ({
|
|
2797
|
-
name: fk.name,
|
|
2798
|
-
columns: Array.isArray(fk.columns) ? fk.columns : [],
|
|
2799
|
-
refSchema: fk.ref_schema,
|
|
2800
|
-
refTable: fk.ref_table,
|
|
2801
|
-
refColumns: Array.isArray(fk.ref_columns) ? fk.ref_columns : []
|
|
2802
|
-
}));
|
|
2803
|
-
const schema = {
|
|
2804
|
-
name: tableName,
|
|
2805
|
-
schema: "public",
|
|
2806
|
-
columns: columns.map((col) => ({
|
|
2807
|
-
name: col.name,
|
|
2808
|
-
type: col.type,
|
|
2809
|
-
nullable: col.nullable,
|
|
2810
|
-
default: col.default_value || undefined,
|
|
2811
|
-
primaryKey: col.is_primary_key,
|
|
2812
|
-
foreignKey: fkMap.get(col.name),
|
|
2813
|
-
autoIncrement: col.auto_increment,
|
|
2814
|
-
comment: col.comment ? fixDoubleEncodedUtf8(col.comment) : null,
|
|
2815
|
-
enumValues: enumMap.get(col.name)
|
|
2816
|
-
})),
|
|
2817
|
-
rowCount: exactRowCount ?? Math.max(0, estimateResults[0]?.estimated_rows || 0),
|
|
2818
|
-
engine: "PostgreSQL",
|
|
2819
|
-
primaryKey: primaryKeyArray,
|
|
2820
|
-
foreignKeys: safeForeignKeys,
|
|
2821
|
-
indexes: indexResults.map((idx) => ({
|
|
2822
|
-
name: idx.name,
|
|
2823
|
-
columns: Array.isArray(idx.columns) ? idx.columns : [],
|
|
2824
|
-
unique: idx.is_unique
|
|
2825
|
-
})),
|
|
2826
|
-
estimatedRowCount: Math.max(0, estimateResults[0]?.estimated_rows || 0)
|
|
2827
|
-
};
|
|
2828
|
-
return schema;
|
|
2829
|
-
});
|
|
2830
|
-
}
|
|
2831
|
-
}
|
|
2832
|
-
|
|
2833
|
-
// src/adapters/mysql-adapter.ts
|
|
2834
|
-
function parseEnumValues(columnType) {
|
|
2835
|
-
const match = columnType.match(/^enum\((.+)\)$/i);
|
|
2836
|
-
if (!match)
|
|
2837
|
-
return;
|
|
2838
|
-
return (match[1] ?? "").split(",").map((v) => v.trim().replace(/^'|'$/g, ""));
|
|
2839
|
-
}
|
|
2840
|
-
|
|
2841
|
-
class MySQLAdapter {
|
|
2842
|
-
db = null;
|
|
2843
|
-
options;
|
|
2844
|
-
system;
|
|
2845
|
-
constructor(options) {
|
|
2846
|
-
this.options = options;
|
|
2847
|
-
this.system = options.system === "mariadb" ? "mariadb" : "mysql";
|
|
2848
|
-
if (options.port < 1 || options.port > 65535) {
|
|
2849
|
-
throw new Error(`Invalid port number: ${options.port}`);
|
|
2850
|
-
}
|
|
2851
|
-
}
|
|
2852
|
-
async connect() {
|
|
2853
|
-
await withMappedConnectionError(this.system, this.options, async () => {
|
|
2854
|
-
const timeouts = resolveTimeoutPolicy(this.options);
|
|
2855
|
-
const { default: driver } = await import("mysql2/promise");
|
|
2856
|
-
this.db = await driver.createConnection({
|
|
2857
|
-
host: this.options.host,
|
|
2858
|
-
port: this.options.port,
|
|
2859
|
-
user: this.options.user,
|
|
2860
|
-
password: this.options.password || undefined,
|
|
2861
|
-
database: this.options.database,
|
|
2862
|
-
charset: "utf8mb4",
|
|
2863
|
-
connectTimeout: timeouts.connectMs
|
|
2864
|
-
});
|
|
2865
|
-
await this.db.execute("SET NAMES utf8mb4");
|
|
2866
|
-
if (timeouts.statementMs !== undefined) {
|
|
2867
|
-
try {
|
|
2868
|
-
await this.db.query(statementTimeoutSql(this.system, timeouts.statementMs));
|
|
2869
|
-
} catch {}
|
|
2870
|
-
}
|
|
2871
|
-
await this.testConnection();
|
|
2872
|
-
try {
|
|
2873
|
-
const rawVersion = await this.getServerVersion();
|
|
2874
|
-
const result = checkDbVersion(rawVersion, this.system);
|
|
2875
|
-
warnIfUnsupported(result);
|
|
2876
|
-
} catch {}
|
|
2877
|
-
});
|
|
2878
|
-
}
|
|
2879
|
-
async disconnect() {
|
|
2880
|
-
try {
|
|
2881
|
-
if (this.db) {
|
|
2882
|
-
await this.db.end();
|
|
2883
|
-
this.db = null;
|
|
2884
|
-
}
|
|
2885
|
-
} catch {}
|
|
2886
|
-
}
|
|
2887
|
-
async testConnection() {
|
|
2888
|
-
requireConnected(this.db);
|
|
2889
|
-
return withMappedConnectionError(this.system, this.options, async () => {
|
|
2890
|
-
const result = await this.execute("SELECT 1 as count");
|
|
2891
|
-
return result.rows.length > 0;
|
|
2892
|
-
});
|
|
2893
|
-
}
|
|
2894
|
-
async execute(sql, params) {
|
|
2895
|
-
const db = requireConnected(this.db);
|
|
2896
|
-
return withMappedConnectionError(this.system, this.options, async () => {
|
|
2897
|
-
const [result] = params ? await db.execute(sql, params) : await db.execute(sql);
|
|
2898
|
-
if (Array.isArray(result)) {
|
|
2899
|
-
return {
|
|
2900
|
-
rows: result,
|
|
2901
|
-
affectedRows: result.length
|
|
2902
|
-
};
|
|
2903
|
-
}
|
|
2904
|
-
const header = result;
|
|
2905
|
-
return {
|
|
2906
|
-
rows: [],
|
|
2907
|
-
affectedRows: header.affectedRows || 0,
|
|
2908
|
-
lastInsertId: header.insertId
|
|
2909
|
-
};
|
|
2910
|
-
});
|
|
2911
|
-
}
|
|
2912
|
-
async getServerVersion() {
|
|
2913
|
-
const result = await this.execute("SELECT VERSION() as version");
|
|
2914
|
-
return result.rows[0]?.version ?? "unknown";
|
|
2915
|
-
}
|
|
2916
|
-
async listTables() {
|
|
2917
|
-
requireConnected(this.db);
|
|
2918
|
-
return withMappedConnectionError(this.system, this.options, async () => {
|
|
2919
|
-
const query = `
|
|
2920
|
-
SELECT
|
|
2921
|
-
t.TABLE_NAME as table_name,
|
|
2922
|
-
t.TABLE_ROWS as row_count,
|
|
2923
|
-
t.ENGINE as engine,
|
|
2924
|
-
t.TABLE_TYPE as table_type,
|
|
2925
|
-
COUNT(c.COLUMN_NAME) as column_count
|
|
2926
|
-
FROM information_schema.TABLES t
|
|
2927
|
-
LEFT JOIN information_schema.COLUMNS c
|
|
2928
|
-
ON t.TABLE_SCHEMA = c.TABLE_SCHEMA AND t.TABLE_NAME = c.TABLE_NAME
|
|
2929
|
-
WHERE t.TABLE_SCHEMA = DATABASE()
|
|
2930
|
-
GROUP BY t.TABLE_NAME, t.TABLE_ROWS, t.ENGINE, t.TABLE_TYPE
|
|
2931
|
-
ORDER BY t.TABLE_NAME
|
|
2932
|
-
`;
|
|
2933
|
-
const result = await this.execute(query);
|
|
2934
|
-
return result.rows.map((row) => ({
|
|
2935
|
-
name: row.table_name,
|
|
2936
|
-
columns: [],
|
|
2937
|
-
columnCount: row.column_count,
|
|
2938
|
-
rowCount: row.row_count || 0,
|
|
2939
|
-
engine: row.engine,
|
|
2940
|
-
estimatedRowCount: row.row_count || 0,
|
|
2941
|
-
tableType: row.table_type === "VIEW" ? "view" : "table"
|
|
2942
|
-
}));
|
|
2943
|
-
});
|
|
2944
|
-
}
|
|
2945
|
-
async getTableSchema(tableName, options) {
|
|
2946
|
-
requireConnected(this.db);
|
|
2947
|
-
return withMappedConnectionError(this.system, this.options, async () => {
|
|
2948
|
-
const columnQuery = `
|
|
2949
|
-
SELECT
|
|
2950
|
-
COLUMN_NAME as name,
|
|
2951
|
-
COLUMN_TYPE as type,
|
|
2952
|
-
IS_NULLABLE = 'YES' as nullable,
|
|
2953
|
-
COLUMN_DEFAULT as default_value,
|
|
2954
|
-
COLUMN_KEY = 'PRI' as is_primary_key,
|
|
2955
|
-
EXTRA LIKE '%auto_increment%' as auto_increment,
|
|
2956
|
-
COLUMN_COMMENT as comment
|
|
2957
|
-
FROM information_schema.COLUMNS
|
|
2958
|
-
WHERE TABLE_SCHEMA = DATABASE()
|
|
2959
|
-
AND TABLE_NAME = ?
|
|
2960
|
-
ORDER BY ORDINAL_POSITION
|
|
2961
|
-
`;
|
|
2962
|
-
const columnResult = await this.execute(columnQuery, [tableName]);
|
|
2963
|
-
const columns = columnResult.rows;
|
|
2964
|
-
const fkQuery = `
|
|
2965
|
-
SELECT
|
|
2966
|
-
rc.CONSTRAINT_NAME as name,
|
|
2967
|
-
GROUP_CONCAT(kcu.COLUMN_NAME) as columns,
|
|
2968
|
-
rc.REFERENCED_TABLE_NAME as ref_table,
|
|
2969
|
-
GROUP_CONCAT(kcu.REFERENCED_COLUMN_NAME) as ref_columns
|
|
2970
|
-
FROM information_schema.REFERENTIAL_CONSTRAINTS rc
|
|
2971
|
-
JOIN information_schema.KEY_COLUMN_USAGE kcu
|
|
2972
|
-
ON rc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME AND rc.TABLE_NAME = kcu.TABLE_NAME
|
|
2973
|
-
WHERE kcu.TABLE_NAME = ? AND rc.CONSTRAINT_SCHEMA = DATABASE()
|
|
2974
|
-
GROUP BY rc.CONSTRAINT_NAME, rc.REFERENCED_TABLE_NAME
|
|
2975
|
-
`;
|
|
2976
|
-
const fkResult = await this.execute(fkQuery, [tableName]);
|
|
2977
|
-
const fkResults = fkResult.rows;
|
|
2978
|
-
const fkMap = new Map;
|
|
2979
|
-
for (const fk of fkResults) {
|
|
2980
|
-
const cols = fk.columns.split(",").map((c) => c.trim());
|
|
2981
|
-
const refCols = fk.ref_columns.split(",").map((c) => c.trim());
|
|
2982
|
-
if (cols.length === 1 && refCols.length === 1) {
|
|
2983
|
-
fkMap.set(cols[0], { table: fk.ref_table, column: refCols[0] });
|
|
2984
|
-
}
|
|
2985
|
-
}
|
|
2986
|
-
const primaryKeyColumns = columns.filter((col) => col.is_primary_key).map((col) => col.name);
|
|
2987
|
-
const indexQuery = `
|
|
2988
|
-
SELECT
|
|
2989
|
-
INDEX_NAME as name,
|
|
2990
|
-
GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) as columns,
|
|
2991
|
-
NOT NON_UNIQUE as is_unique
|
|
2992
|
-
FROM information_schema.STATISTICS
|
|
2993
|
-
WHERE TABLE_SCHEMA = DATABASE()
|
|
2994
|
-
AND TABLE_NAME = ?
|
|
2995
|
-
AND INDEX_NAME != 'PRIMARY'
|
|
2996
|
-
GROUP BY INDEX_NAME, NON_UNIQUE
|
|
2997
|
-
`;
|
|
2998
|
-
const indexResult = await this.execute(indexQuery, [tableName]);
|
|
2999
|
-
const indexResults = indexResult.rows;
|
|
3000
|
-
const tableMetaQuery = `
|
|
3001
|
-
SELECT ENGINE as engine, TABLE_ROWS as estimated_rows
|
|
3002
|
-
FROM information_schema.TABLES
|
|
3003
|
-
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?
|
|
3004
|
-
`;
|
|
3005
|
-
const tableMetaResult = await this.execute(tableMetaQuery, [tableName]);
|
|
3006
|
-
const tableMeta = tableMetaResult.rows[0];
|
|
3007
|
-
const estimatedRowCount = Math.max(0, tableMeta?.estimated_rows || 0);
|
|
3008
|
-
const exactRowCount = options?.exactRowCount === false ? undefined : (await this.execute(`SELECT COUNT(*) as count FROM ${quoteIdentifier(tableName, this.system)}`)).rows[0]?.count;
|
|
3009
|
-
const schema = {
|
|
3010
|
-
name: tableName,
|
|
3011
|
-
columns: columns.map((col) => ({
|
|
3012
|
-
name: col.name,
|
|
3013
|
-
type: col.type,
|
|
3014
|
-
nullable: col.nullable,
|
|
3015
|
-
default: col.default_value || undefined,
|
|
3016
|
-
primaryKey: col.is_primary_key,
|
|
3017
|
-
foreignKey: fkMap.get(col.name),
|
|
3018
|
-
autoIncrement: col.auto_increment,
|
|
3019
|
-
comment: col.comment ? fixDoubleEncodedUtf8(col.comment) : null,
|
|
3020
|
-
enumValues: parseEnumValues(col.type)
|
|
3021
|
-
})),
|
|
3022
|
-
rowCount: exactRowCount ?? estimatedRowCount,
|
|
3023
|
-
engine: tableMeta?.engine || "MySQL",
|
|
3024
|
-
primaryKey: primaryKeyColumns,
|
|
3025
|
-
foreignKeys: fkResults.map((fk) => ({
|
|
3026
|
-
name: fk.name,
|
|
3027
|
-
columns: fk.columns.split(",").map((c) => c.trim()),
|
|
3028
|
-
refTable: fk.ref_table,
|
|
3029
|
-
refColumns: fk.ref_columns.split(",").map((c) => c.trim())
|
|
3030
|
-
})),
|
|
3031
|
-
indexes: indexResults.map((idx) => ({
|
|
3032
|
-
name: idx.name,
|
|
3033
|
-
columns: idx.columns.split(",").map((c) => c.trim()),
|
|
3034
|
-
unique: idx.is_unique
|
|
3035
|
-
})),
|
|
3036
|
-
estimatedRowCount
|
|
3037
|
-
};
|
|
3038
|
-
return schema;
|
|
3039
|
-
});
|
|
3040
|
-
}
|
|
3041
|
-
}
|
|
3042
|
-
|
|
3043
|
-
// src/adapters/mongodb-adapter.ts
|
|
3044
|
-
import { resolveSrv, resolveTxt } from "dns/promises";
|
|
3045
|
-
|
|
3046
|
-
// src/adapters/server-side-script.ts
|
|
3047
|
-
class ServerSideScriptRejection extends Error {
|
|
3048
|
-
operator;
|
|
3049
|
-
constructor(message, operator) {
|
|
3050
|
-
super(message);
|
|
3051
|
-
this.operator = operator;
|
|
3052
|
-
this.name = "ServerSideScriptRejection";
|
|
3053
|
-
Object.setPrototypeOf(this, ServerSideScriptRejection.prototype);
|
|
3054
|
-
}
|
|
3055
|
-
}
|
|
3056
|
-
var MONGO_SCRIPT_OPERATORS = ["$where", "$function", "$accumulator"];
|
|
3057
|
-
var ES_SCRIPT_KEYS = ["script", "script_fields"];
|
|
3058
|
-
function findKey(value, keys) {
|
|
3059
|
-
if (value === null || typeof value !== "object")
|
|
3060
|
-
return;
|
|
3061
|
-
if (Array.isArray(value)) {
|
|
3062
|
-
for (const item of value) {
|
|
3063
|
-
const found = findKey(item, keys);
|
|
3064
|
-
if (found)
|
|
3065
|
-
return found;
|
|
3066
|
-
}
|
|
3067
|
-
return;
|
|
3068
|
-
}
|
|
3069
|
-
const record = value;
|
|
3070
|
-
for (const key of keys) {
|
|
3071
|
-
if (key in record)
|
|
3072
|
-
return key;
|
|
3073
|
-
}
|
|
3074
|
-
for (const nested of Object.values(record)) {
|
|
3075
|
-
const found = findKey(nested, keys);
|
|
3076
|
-
if (found)
|
|
3077
|
-
return found;
|
|
3078
|
-
}
|
|
3079
|
-
return;
|
|
3080
|
-
}
|
|
3081
|
-
function assertNoMongoServerSideScript(query) {
|
|
3082
|
-
const operator = findKey(query, MONGO_SCRIPT_OPERATORS);
|
|
3083
|
-
if (!operator)
|
|
3084
|
-
return;
|
|
3085
|
-
throw new ServerSideScriptRejection(`MongoDB server-side script rejected: '${operator}' executes JavaScript on the database server. ` + `Rewrite the query with standard operators, or run it through a tool that is meant to allow it.`, operator);
|
|
3086
|
-
}
|
|
3087
|
-
function assertNoElasticsearchScript(body) {
|
|
3088
|
-
const key = findKey(body, ES_SCRIPT_KEYS);
|
|
3089
|
-
if (!key)
|
|
3090
|
-
return;
|
|
3091
|
-
throw new ServerSideScriptRejection(`Elasticsearch server-side script rejected: '${key}' executes script code on the cluster. ` + `Rewrite the query without it.`, key);
|
|
3092
|
-
}
|
|
3093
|
-
|
|
3094
|
-
// src/adapters/mongodb-adapter.ts
|
|
3095
|
-
class MongoDBAdapter {
|
|
3096
|
-
options;
|
|
3097
|
-
ClientClass;
|
|
3098
|
-
client = null;
|
|
3099
|
-
constructor(options, ClientClass = null) {
|
|
3100
|
-
this.options = options;
|
|
3101
|
-
this.ClientClass = ClientClass;
|
|
3102
|
-
}
|
|
3103
|
-
async resolveClientClass() {
|
|
3104
|
-
if (this.ClientClass)
|
|
3105
|
-
return this.ClientClass;
|
|
3106
|
-
const { MongoClient } = await import("mongodb");
|
|
3107
|
-
return MongoClient;
|
|
3108
|
-
}
|
|
3109
|
-
buildUri() {
|
|
3110
|
-
if (this.options.uri)
|
|
3111
|
-
return this.options.uri;
|
|
3112
|
-
const { user, password, host, port, database, authSource, replicaSet, tls, srv } = this.options;
|
|
3113
|
-
if (!host) {
|
|
3114
|
-
throw new ConnectionError("UNKNOWN", "MongoDB host \u672A\u8A2D\u5B9A", [
|
|
3115
|
-
"\u8ACB\u586B\u5BEB host\uFF0C\u6216\u6539\u7528 uri \u6B04\u4F4D\u6307\u5B9A\u5B8C\u6574\u9023\u7DDA\u5B57\u4E32"
|
|
3116
|
-
]);
|
|
3117
|
-
}
|
|
3118
|
-
const isBracketedIpv6 = /^\[[0-9a-f:]+\]$/i.test(host);
|
|
3119
|
-
if (!isBracketedIpv6 && /[/@?#:\s\\]/.test(host)) {
|
|
3120
|
-
throw new ConnectionError("UNKNOWN", `MongoDB host \u542B\u6709\u975E\u6CD5\u5B57\u5143: ${host}`, [
|
|
3121
|
-
"host \u53EA\u61C9\u5305\u542B\u4E3B\u6A5F\u540D\u7A31\u6216 IP\uFF0C\u4E0D\u8981\u542B /\u3001@\u3001?\u3001#\u3001: \u6216\u7A7A\u767D",
|
|
3122
|
-
"\u57E0\u865F\u8ACB\u586B\u5728 port \u6B04\u4F4D\uFF0C\u4E0D\u8981\u4F75\u9032 host",
|
|
3123
|
-
"IPv6 \u4F4D\u5740\u8ACB\u52A0\u65B9\u62EC\u865F\uFF0C\u4F8B\u5982 [::1]",
|
|
3124
|
-
"\u82E5\u8981\u6307\u5B9A\u5B8C\u6574\u9023\u7DDA\u5B57\u4E32\uFF0C\u8ACB\u6539\u7528 uri \u6B04\u4F4D"
|
|
3125
|
-
]);
|
|
3126
|
-
}
|
|
3127
|
-
if (user && !password) {
|
|
3128
|
-
throw new ConnectionError("UNKNOWN", "\u5DF2\u6307\u5B9A user \u4F46\u672A\u63D0\u4F9B password", [
|
|
3129
|
-
'\u8ACB\u88DC\u4E0A password\uFF0C\u6216\u6539\u7528\u74B0\u5883\u8B8A\u6578\u53C3\u7167 {"$env": "..."}',
|
|
3130
|
-
"\u82E5\u78BA\u5B9A\u8981\u4EE5\u7121\u8A8D\u8B49\u65B9\u5F0F\u9023\u7DDA\uFF0C\u8ACB\u4E00\u4F75\u6E05\u7A7A user"
|
|
3131
|
-
]);
|
|
3132
|
-
}
|
|
3133
|
-
const userInfo = user ? `${encodeURIComponent(user)}:${encodeURIComponent(password)}@` : "";
|
|
3134
|
-
const scheme = srv ? "mongodb+srv://" : "mongodb://";
|
|
3135
|
-
const authority = srv ? host : `${host}:${port}`;
|
|
3136
|
-
const path = database ? `/${encodeURIComponent(database)}` : "/";
|
|
3137
|
-
const query = new URLSearchParams;
|
|
3138
|
-
if (user)
|
|
3139
|
-
query.set("authSource", authSource || "admin");
|
|
3140
|
-
if (replicaSet)
|
|
3141
|
-
query.set("replicaSet", replicaSet);
|
|
3142
|
-
if (tls !== undefined)
|
|
3143
|
-
query.set("tls", String(tls));
|
|
3144
|
-
const search = query.toString();
|
|
3145
|
-
return `${scheme}${userInfo}${authority}${path}${search ? `?${search}` : ""}`;
|
|
3146
|
-
}
|
|
3147
|
-
parseTxtRecords(records) {
|
|
3148
|
-
const combined = records.flat().map((record) => record.replace(/^"|"$/g, "")).join("&");
|
|
3149
|
-
if (!combined)
|
|
3150
|
-
return {};
|
|
3151
|
-
const params = new URLSearchParams(combined);
|
|
3152
|
-
return Object.fromEntries(params.entries());
|
|
3153
|
-
}
|
|
3154
|
-
async resolveSrvHosts(hostname) {
|
|
3155
|
-
const srvName = `_mongodb._tcp.${hostname}`;
|
|
3156
|
-
try {
|
|
3157
|
-
const records = await resolveSrv(srvName);
|
|
3158
|
-
if (!records.length) {
|
|
3159
|
-
throw new Error(`No SRV records found for ${srvName}`);
|
|
3160
|
-
}
|
|
3161
|
-
return records.map((record) => {
|
|
3162
|
-
const target = String(record.name || record.target || "").replace(/\.$/, "");
|
|
3163
|
-
return `${target}:${record.port}`;
|
|
3164
|
-
});
|
|
3165
|
-
} catch (error) {
|
|
3166
|
-
const code = error?.code;
|
|
3167
|
-
if (!["ECONNREFUSED", "ENOTFOUND", "ETIMEOUT", "ETIMEDOUT"].includes(code ?? "")) {
|
|
3168
|
-
throw error;
|
|
3169
|
-
}
|
|
3170
|
-
}
|
|
3171
|
-
const response = await fetch(`https://dns.google/resolve?name=${encodeURIComponent(srvName)}&type=SRV`);
|
|
3172
|
-
if (!response.ok) {
|
|
3173
|
-
throw new Error(`SRV lookup failed with HTTP ${response.status}`);
|
|
3174
|
-
}
|
|
3175
|
-
const payload = await response.json();
|
|
3176
|
-
if (payload.Status !== 0 || !payload.Answer?.length) {
|
|
3177
|
-
throw new Error(payload.Comment || `No SRV records found for ${srvName}`);
|
|
3178
|
-
}
|
|
3179
|
-
return payload.Answer.map((answer) => {
|
|
3180
|
-
const parts = answer.data.trim().split(/\s+/);
|
|
3181
|
-
const port = parts[2];
|
|
3182
|
-
const target = parts.slice(3).join(" ").replace(/\.$/, "");
|
|
3183
|
-
return `${target}:${port}`;
|
|
3184
|
-
});
|
|
3185
|
-
}
|
|
3186
|
-
async resolveTxtOptions(hostname) {
|
|
3187
|
-
try {
|
|
3188
|
-
const records = await resolveTxt(hostname);
|
|
3189
|
-
return this.parseTxtRecords(records);
|
|
3190
|
-
} catch (error) {
|
|
3191
|
-
const code = error?.code;
|
|
3192
|
-
if (!["ECONNREFUSED", "ENOTFOUND", "ETIMEOUT", "ETIMEDOUT"].includes(code ?? "")) {
|
|
3193
|
-
throw error;
|
|
3194
|
-
}
|
|
3195
|
-
}
|
|
3196
|
-
const response = await fetch(`https://dns.google/resolve?name=${encodeURIComponent(hostname)}&type=TXT`);
|
|
3197
|
-
if (!response.ok) {
|
|
3198
|
-
throw new Error(`TXT lookup failed with HTTP ${response.status}`);
|
|
3199
|
-
}
|
|
3200
|
-
const payload = await response.json();
|
|
3201
|
-
if (payload.Status !== 0 || !payload.Answer?.length) {
|
|
3202
|
-
return {};
|
|
3203
|
-
}
|
|
3204
|
-
return this.parseTxtRecords(payload.Answer.map((answer) => [answer.data]));
|
|
3205
|
-
}
|
|
3206
|
-
async buildResolvedUri() {
|
|
3207
|
-
const canonical = this.buildUri();
|
|
3208
|
-
if (!canonical.startsWith("mongodb+srv://")) {
|
|
3209
|
-
return canonical;
|
|
3210
|
-
}
|
|
3211
|
-
const url = new URL(canonical);
|
|
3212
|
-
const hosts = await this.resolveSrvHosts(url.hostname);
|
|
3213
|
-
const txtOptions = await this.resolveTxtOptions(url.hostname);
|
|
3214
|
-
const query = new URLSearchParams(url.searchParams);
|
|
3215
|
-
for (const [key, value] of Object.entries(txtOptions)) {
|
|
3216
|
-
if (!query.has(key)) {
|
|
3217
|
-
query.set(key, value);
|
|
3218
|
-
}
|
|
3219
|
-
}
|
|
3220
|
-
if (!query.has("tls") && !query.has("ssl")) {
|
|
3221
|
-
query.set("tls", "true");
|
|
3222
|
-
}
|
|
3223
|
-
if ((url.username || url.password) && !query.has("authSource")) {
|
|
3224
|
-
query.set("authSource", "admin");
|
|
3225
|
-
}
|
|
3226
|
-
const userInfo = url.username || url.password ? `${encodeURIComponent(decodeURIComponent(url.username))}${url.password ? `:${encodeURIComponent(decodeURIComponent(url.password))}` : ""}@` : "";
|
|
3227
|
-
const path = url.pathname && url.pathname !== "/" ? url.pathname : `/${this.options.database || "test"}`;
|
|
3228
|
-
const search = query.toString();
|
|
3229
|
-
return `mongodb://${userInfo}${hosts.join(",")}${path}${search ? `?${search}` : ""}`;
|
|
3230
|
-
}
|
|
3231
|
-
connectionHints(error, message) {
|
|
3232
|
-
const AUTH_HINTS = [
|
|
3233
|
-
"\u8A8D\u8B49\u5931\u6557\uFF1A\u8ACB\u78BA\u8A8D user / password \u6B63\u78BA",
|
|
3234
|
-
"\u8ACB\u78BA\u8A8D authSource \u6307\u5411\u5B58\u653E\u8A72\u5E33\u865F\u7684\u8CC7\u6599\u5EAB\uFF08Atlas \u8207\u591A\u6578\u81EA\u67B6\u74B0\u5883\u70BA admin\uFF09"
|
|
3235
|
-
];
|
|
3236
|
-
const DNS_HINTS = [
|
|
3237
|
-
"DNS/SRV \u89E3\u6790\u5931\u6557\uFF1A\u8ACB\u78BA\u8A8D host \u70BA SRV \u7DB2\u57DF\uFF0C\u4E14 srv \u8A2D\u5B9A\u8207\u5B83\u4E00\u81F4",
|
|
3238
|
-
"\u82E5\u8A72\u4E3B\u6A5F\u4E0D\u662F SRV \u7DB2\u57DF\uFF0C\u8ACB\u95DC\u9589 srv \u4E26\u6539\u586B host \u8207 port",
|
|
3239
|
-
"\u8ACB\u78BA\u8A8D\u672C\u6A5F DNS \u6216\u7DB2\u8DEF\uFF08VPN\u3001\u516C\u53F8\u7DB2\u8DEF\uFF09\u5141\u8A31 SRV \u67E5\u8A62"
|
|
3240
|
-
];
|
|
3241
|
-
const TLS_HINTS = [
|
|
3242
|
-
"TLS \u63E1\u624B\u5931\u6557\uFF1A\u8ACB\u78BA\u8A8D tls \u6B04\u4F4D\u8A2D\u5B9A\u8207\u4F3A\u670D\u5668\u4E00\u81F4",
|
|
3243
|
-
"\u81EA\u7C3D\u6191\u8B49\u74B0\u5883\u9700\u8981\u5728\u4F3A\u670D\u5668\u7AEF\u6216\u7CFB\u7D71\u4FE1\u4EFB\u93C8\u4E2D\u5B89\u88DD CA \u6191\u8B49"
|
|
3244
|
-
];
|
|
3245
|
-
const err = error;
|
|
3246
|
-
const causeCode = String(err?.cause?.code ?? err?.code ?? "");
|
|
3247
|
-
if (err?.code === 18 || err?.codeName === "AuthenticationFailed")
|
|
3248
|
-
return AUTH_HINTS;
|
|
3249
|
-
if (["ENOTFOUND", "EAI_AGAIN"].includes(causeCode))
|
|
3250
|
-
return DNS_HINTS;
|
|
3251
|
-
if (causeCode.startsWith("ERR_TLS") || causeCode.startsWith("SELF_SIGNED"))
|
|
3252
|
-
return TLS_HINTS;
|
|
3253
|
-
if (/authentication failed|not authorized|bad auth/i.test(message))
|
|
3254
|
-
return AUTH_HINTS;
|
|
3255
|
-
if (/querySrv|getaddrinfo (ENOTFOUND|EAI_AGAIN)/i.test(message))
|
|
3256
|
-
return DNS_HINTS;
|
|
3257
|
-
if (/unable to verify the first certificate|self.signed certificate|certificate has expired|ERR_TLS/i.test(message)) {
|
|
3258
|
-
return TLS_HINTS;
|
|
3259
|
-
}
|
|
3260
|
-
return ["\u8ACB\u78BA\u8A8D MongoDB \u670D\u52D9\u6B63\u5728\u57F7\u884C", "\u8ACB\u78BA\u8A8D\u9023\u7DDA\u8A2D\u5B9A\uFF08URI \u6216 host/port\uFF09\u6B63\u78BA"];
|
|
3261
|
-
}
|
|
3262
|
-
getDatabase() {
|
|
3263
|
-
if (!this.client) {
|
|
3264
|
-
throw new ConnectionError("UNKNOWN", "\u5C1A\u672A\u9023\u7DDA\uFF0C\u8ACB\u5148\u547C\u53EB connect()", []);
|
|
3265
|
-
}
|
|
3266
|
-
return this.client.db(this.options.database || undefined);
|
|
3267
|
-
}
|
|
3268
|
-
async connect() {
|
|
3269
|
-
const ClientClass = await this.resolveClientClass();
|
|
3270
|
-
const uri = await this.buildResolvedUri();
|
|
3271
|
-
try {
|
|
3272
|
-
this.client = new ClientClass(uri, { serverSelectionTimeoutMS: this.options.timeout ?? 5000 });
|
|
3273
|
-
await this.client.connect();
|
|
3274
|
-
} catch (err) {
|
|
3275
|
-
const message = err.message ?? "Unknown error";
|
|
3276
|
-
const code = message.includes("ECONNREFUSED") ? "ECONNREFUSED" : message.includes("ETIMEDOUT") ? "ETIMEDOUT" : "UNKNOWN";
|
|
3277
|
-
throw new ConnectionError(code, `MongoDB \u9023\u7DDA\u5931\u6557: ${message}`, this.connectionHints(err, message));
|
|
3278
|
-
}
|
|
3279
|
-
}
|
|
3280
|
-
async disconnect() {
|
|
3281
|
-
if (this.client) {
|
|
3282
|
-
await this.client.close();
|
|
3283
|
-
this.client = null;
|
|
3284
|
-
}
|
|
3285
|
-
}
|
|
3286
|
-
async execute(query, params, options) {
|
|
3287
|
-
const db = this.getDatabase();
|
|
3288
|
-
const collectionName = params?.[0];
|
|
3289
|
-
const collection = db.collection(collectionName);
|
|
3290
|
-
let parsed;
|
|
3291
|
-
try {
|
|
3292
|
-
parsed = JSON.parse(query);
|
|
3293
|
-
} catch {
|
|
3294
|
-
throw new Error("MongoDB \u67E5\u8A62\u5FC5\u9808\u662F\u6709\u6548\u7684 JSON\uFF08object filter \u6216 array pipeline\uFF09");
|
|
3295
|
-
}
|
|
3296
|
-
assertNoMongoServerSideScript(parsed);
|
|
3297
|
-
const limit = options?.limit;
|
|
3298
|
-
let docs;
|
|
3299
|
-
if (Array.isArray(parsed)) {
|
|
3300
|
-
const stages = parsed;
|
|
3301
|
-
const alreadyHasLimit = stages.some((stage) => stage !== null && typeof stage === "object" && ("$limit" in stage));
|
|
3302
|
-
const limitedStages = typeof limit === "number" && limit > 0 && !alreadyHasLimit ? [...stages, { $limit: limit }] : stages;
|
|
3303
|
-
const finalStages = options?.projection ? [...limitedStages, { $project: options.projection }] : limitedStages;
|
|
3304
|
-
docs = await collection.aggregate(finalStages).toArray();
|
|
3305
|
-
} else {
|
|
3306
|
-
const cap2 = typeof limit === "number" && limit > 0 ? limit : 0;
|
|
3307
|
-
const cursor = options?.projection ? collection.find(parsed, { projection: options.projection }) : collection.find(parsed);
|
|
3308
|
-
docs = await cursor.limit(cap2).toArray();
|
|
3309
|
-
}
|
|
3310
|
-
return { rows: docs, affectedRows: docs.length };
|
|
3311
|
-
}
|
|
3312
|
-
async listCollections() {
|
|
3313
|
-
const db = this.getDatabase();
|
|
3314
|
-
const colls = await db.listCollections().toArray();
|
|
3315
|
-
const results = await Promise.all(colls.map(async (col) => {
|
|
3316
|
-
try {
|
|
3317
|
-
const count = await db.collection(col.name).estimatedDocumentCount();
|
|
3318
|
-
return { name: col.name, documentCount: count };
|
|
3319
|
-
} catch {
|
|
3320
|
-
return { name: col.name };
|
|
3321
|
-
}
|
|
3322
|
-
}));
|
|
3323
|
-
return results;
|
|
3324
|
-
}
|
|
3325
|
-
async listTables() {
|
|
3326
|
-
const collections = await this.listCollections();
|
|
3327
|
-
return collections.map((c) => ({
|
|
3328
|
-
name: c.name,
|
|
3329
|
-
estimatedRowCount: c.documentCount,
|
|
3330
|
-
columns: []
|
|
3331
|
-
}));
|
|
3332
|
-
}
|
|
3333
|
-
async getTableSchema(collectionName, options) {
|
|
3334
|
-
const db = this.getDatabase();
|
|
3335
|
-
const collection = db.collection(collectionName);
|
|
3336
|
-
const estimatedRowCount = await collection.estimatedDocumentCount();
|
|
3337
|
-
const requested = options?.sampleSize;
|
|
3338
|
-
const desired = typeof requested === "number" && requested >= 1 ? requested : 100;
|
|
3339
|
-
const sampleSize = Math.min(desired, 1000);
|
|
3340
|
-
const method = options?.sampleMethod ?? "random";
|
|
3341
|
-
let samples = [];
|
|
3342
|
-
if (method === "random") {
|
|
3343
|
-
try {
|
|
3344
|
-
samples = await collection.aggregate([{ $sample: { size: sampleSize } }]).toArray();
|
|
3345
|
-
} catch (err) {
|
|
3346
|
-
console.error(`[mongo] $sample failed (${err.message}); falling back to natural order.`);
|
|
3347
|
-
samples = await collection.find().limit(sampleSize).toArray();
|
|
3348
|
-
}
|
|
3349
|
-
} else {
|
|
3350
|
-
samples = await collection.find().limit(sampleSize).toArray();
|
|
3351
|
-
}
|
|
3352
|
-
const accumulator = new Map;
|
|
3353
|
-
for (const doc of samples)
|
|
3354
|
-
accumulateDocPaths(doc, "", accumulator);
|
|
3355
|
-
const denom = Math.max(samples.length, 1);
|
|
3356
|
-
const columns = Array.from(accumulator.entries()).map(([name, info]) => ({
|
|
3357
|
-
name,
|
|
3358
|
-
type: Array.from(info.types).sort().join(" | "),
|
|
3359
|
-
nullable: info.types.has("null") || info.count < denom,
|
|
3360
|
-
presence: info.count / denom
|
|
3361
|
-
}));
|
|
3362
|
-
return {
|
|
3363
|
-
name: collectionName,
|
|
3364
|
-
columns,
|
|
3365
|
-
estimatedRowCount,
|
|
3366
|
-
tableType: "table"
|
|
3367
|
-
};
|
|
3368
|
-
}
|
|
3369
|
-
async testConnection() {
|
|
3370
|
-
if (!this.client)
|
|
3371
|
-
return false;
|
|
3372
|
-
await this.getDatabase().command({ ping: 1 });
|
|
3373
|
-
return true;
|
|
3374
|
-
}
|
|
3375
|
-
async getServerVersion() {
|
|
3376
|
-
const info = await this.getDatabase().admin().serverInfo();
|
|
3377
|
-
return info.version ?? "unknown";
|
|
3378
|
-
}
|
|
3379
|
-
async insert(collection, data) {
|
|
3380
|
-
const db = this.getDatabase();
|
|
3381
|
-
const result = await db.collection(collection).insertOne(data);
|
|
3382
|
-
return {
|
|
3383
|
-
rows: [],
|
|
3384
|
-
affectedRows: result.acknowledged ? 1 : 0,
|
|
3385
|
-
lastInsertId: result.insertedId.toString()
|
|
3386
|
-
};
|
|
3387
|
-
}
|
|
3388
|
-
async update(collection, filter, update) {
|
|
3389
|
-
const db = this.getDatabase();
|
|
3390
|
-
assertNoMongoServerSideScript(filter);
|
|
3391
|
-
assertNoMongoServerSideScript(update);
|
|
3392
|
-
const result = await db.collection(collection).updateMany(filter, update);
|
|
3393
|
-
return {
|
|
3394
|
-
rows: [],
|
|
3395
|
-
affectedRows: result.modifiedCount
|
|
3396
|
-
};
|
|
3397
|
-
}
|
|
3398
|
-
async delete(collection, filter) {
|
|
3399
|
-
const db = this.getDatabase();
|
|
3400
|
-
assertNoMongoServerSideScript(filter);
|
|
3401
|
-
const result = await db.collection(collection).deleteMany(filter);
|
|
3402
|
-
return {
|
|
3403
|
-
rows: [],
|
|
3404
|
-
affectedRows: result.deletedCount
|
|
3405
|
-
};
|
|
3406
|
-
}
|
|
3407
|
-
}
|
|
3408
|
-
function detectMongoType(v) {
|
|
3409
|
-
if (v === null || v === undefined)
|
|
3410
|
-
return "null";
|
|
3411
|
-
if (Array.isArray(v))
|
|
3412
|
-
return "array";
|
|
3413
|
-
if (v instanceof Date)
|
|
3414
|
-
return "Date";
|
|
3415
|
-
if (typeof v === "object") {
|
|
3416
|
-
const ctor = v.constructor?.name;
|
|
3417
|
-
if (ctor === "ObjectId")
|
|
3418
|
-
return "ObjectId";
|
|
3419
|
-
if (ctor === "Binary")
|
|
3420
|
-
return "Binary";
|
|
3421
|
-
if (ctor === "Decimal128")
|
|
3422
|
-
return "Decimal128";
|
|
3423
|
-
return "object";
|
|
3424
|
-
}
|
|
3425
|
-
if (typeof v === "string")
|
|
3426
|
-
return "string";
|
|
3427
|
-
if (typeof v === "number")
|
|
3428
|
-
return "number";
|
|
3429
|
-
if (typeof v === "boolean")
|
|
3430
|
-
return "boolean";
|
|
3431
|
-
return "string";
|
|
3432
|
-
}
|
|
3433
|
-
function accumulateDocPaths(doc, prefix, acc) {
|
|
3434
|
-
for (const [k, v] of Object.entries(doc)) {
|
|
3435
|
-
const path = prefix === "" ? k : `${prefix}.${k}`;
|
|
3436
|
-
const t2 = detectMongoType(v);
|
|
3437
|
-
const slot = acc.get(path) ?? { types: new Set, count: 0 };
|
|
3438
|
-
slot.types.add(t2);
|
|
3439
|
-
slot.count += 1;
|
|
3440
|
-
acc.set(path, slot);
|
|
3441
|
-
if (t2 === "object" && v && typeof v === "object") {
|
|
3442
|
-
accumulateDocPaths(v, path, acc);
|
|
3443
|
-
}
|
|
3444
|
-
}
|
|
3445
|
-
}
|
|
2027
|
+
// src/core/mongo/request-fields.ts
|
|
2028
|
+
var WHOLE_DOCUMENT_VARIABLES = new Set(["ROOT", "CURRENT"]);
|
|
2029
|
+
var RESHAPES_DOCUMENT = new Set(["$objectToArray", "$replaceRoot", "$replaceWith"]);
|
|
3446
2030
|
|
|
3447
2031
|
// src/adapters/redis/command-metadata.ts
|
|
3448
2032
|
var tbl = (s) => s;
|
|
@@ -3450,465 +2034,439 @@ var REDIS_COMMAND_TABLE = Object.freeze({
|
|
|
3450
2034
|
PING: tbl({
|
|
3451
2035
|
readOnly: true,
|
|
3452
2036
|
sizeGuard: { kind: "unbounded" },
|
|
3453
|
-
keyArity: { kind: "no-key" }
|
|
3454
|
-
permissionTier: "query-only"
|
|
2037
|
+
keyArity: { kind: "no-key" }
|
|
3455
2038
|
}),
|
|
3456
2039
|
INFO: tbl({
|
|
3457
2040
|
readOnly: true,
|
|
3458
2041
|
sizeGuard: { kind: "unbounded" },
|
|
3459
|
-
keyArity: { kind: "no-key" }
|
|
3460
|
-
permissionTier: "query-only"
|
|
2042
|
+
keyArity: { kind: "no-key" }
|
|
3461
2043
|
}),
|
|
3462
2044
|
DBSIZE: tbl({
|
|
3463
2045
|
readOnly: true,
|
|
3464
2046
|
sizeGuard: { kind: "unbounded" },
|
|
3465
|
-
keyArity: { kind: "no-key" }
|
|
3466
|
-
permissionTier: "query-only"
|
|
2047
|
+
keyArity: { kind: "no-key" }
|
|
3467
2048
|
}),
|
|
3468
2049
|
TYPE: tbl({
|
|
3469
2050
|
readOnly: true,
|
|
3470
2051
|
sizeGuard: { kind: "unbounded" },
|
|
3471
|
-
keyArity: { kind: "single", argIndex: 0 }
|
|
3472
|
-
permissionTier: "query-only"
|
|
2052
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
3473
2053
|
}),
|
|
3474
2054
|
EXISTS: tbl({
|
|
3475
2055
|
readOnly: true,
|
|
3476
2056
|
sizeGuard: { kind: "unbounded" },
|
|
3477
|
-
keyArity: { kind: "multi-variable", startIndex: 0, step: 1 }
|
|
3478
|
-
permissionTier: "query-only"
|
|
2057
|
+
keyArity: { kind: "multi-variable", startIndex: 0, step: 1 }
|
|
3479
2058
|
}),
|
|
3480
2059
|
TTL: tbl({
|
|
3481
2060
|
readOnly: true,
|
|
3482
2061
|
sizeGuard: { kind: "unbounded" },
|
|
3483
|
-
keyArity: { kind: "single", argIndex: 0 }
|
|
3484
|
-
permissionTier: "query-only"
|
|
2062
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
3485
2063
|
}),
|
|
3486
2064
|
GET: tbl({
|
|
3487
2065
|
readOnly: true,
|
|
3488
2066
|
sizeGuard: { kind: "unbounded" },
|
|
3489
|
-
keyArity: { kind: "single", argIndex: 0 }
|
|
3490
|
-
permissionTier: "query-only"
|
|
2067
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
3491
2068
|
}),
|
|
3492
2069
|
MGET: tbl({
|
|
3493
2070
|
readOnly: true,
|
|
3494
2071
|
sizeGuard: { kind: "unbounded" },
|
|
3495
|
-
keyArity: { kind: "multi-variable", startIndex: 0, step: 1 }
|
|
3496
|
-
permissionTier: "query-only"
|
|
2072
|
+
keyArity: { kind: "multi-variable", startIndex: 0, step: 1 }
|
|
3497
2073
|
}),
|
|
3498
2074
|
STRLEN: tbl({
|
|
3499
2075
|
readOnly: true,
|
|
3500
2076
|
sizeGuard: { kind: "unbounded" },
|
|
3501
|
-
keyArity: { kind: "single", argIndex: 0 }
|
|
3502
|
-
permissionTier: "query-only"
|
|
2077
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
3503
2078
|
}),
|
|
3504
2079
|
HGET: tbl({
|
|
3505
2080
|
readOnly: true,
|
|
3506
2081
|
sizeGuard: { kind: "unbounded" },
|
|
3507
|
-
keyArity: { kind: "single", argIndex: 0 }
|
|
3508
|
-
permissionTier: "query-only"
|
|
2082
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
3509
2083
|
}),
|
|
3510
2084
|
HEXISTS: tbl({
|
|
3511
2085
|
readOnly: true,
|
|
3512
2086
|
sizeGuard: { kind: "unbounded" },
|
|
3513
|
-
keyArity: { kind: "single", argIndex: 0 }
|
|
3514
|
-
permissionTier: "query-only"
|
|
2087
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
3515
2088
|
}),
|
|
3516
2089
|
HLEN: tbl({
|
|
3517
2090
|
readOnly: true,
|
|
3518
2091
|
sizeGuard: { kind: "unbounded" },
|
|
3519
|
-
keyArity: { kind: "single", argIndex: 0 }
|
|
3520
|
-
permissionTier: "query-only"
|
|
2092
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
3521
2093
|
}),
|
|
3522
2094
|
HMGET: tbl({
|
|
3523
2095
|
readOnly: true,
|
|
3524
2096
|
sizeGuard: { kind: "unbounded" },
|
|
3525
|
-
keyArity: { kind: "single", argIndex: 0 }
|
|
3526
|
-
permissionTier: "query-only"
|
|
2097
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
3527
2098
|
}),
|
|
3528
2099
|
LLEN: tbl({
|
|
3529
2100
|
readOnly: true,
|
|
3530
2101
|
sizeGuard: { kind: "unbounded" },
|
|
3531
|
-
keyArity: { kind: "single", argIndex: 0 }
|
|
3532
|
-
permissionTier: "query-only"
|
|
2102
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
3533
2103
|
}),
|
|
3534
2104
|
LINDEX: tbl({
|
|
3535
2105
|
readOnly: true,
|
|
3536
2106
|
sizeGuard: { kind: "unbounded" },
|
|
3537
|
-
keyArity: { kind: "single", argIndex: 0 }
|
|
3538
|
-
permissionTier: "query-only"
|
|
2107
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
3539
2108
|
}),
|
|
3540
2109
|
SCARD: tbl({
|
|
3541
2110
|
readOnly: true,
|
|
3542
2111
|
sizeGuard: { kind: "unbounded" },
|
|
3543
|
-
keyArity: { kind: "single", argIndex: 0 }
|
|
3544
|
-
permissionTier: "query-only"
|
|
2112
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
3545
2113
|
}),
|
|
3546
2114
|
SISMEMBER: tbl({
|
|
3547
2115
|
readOnly: true,
|
|
3548
2116
|
sizeGuard: { kind: "unbounded" },
|
|
3549
|
-
keyArity: { kind: "single", argIndex: 0 }
|
|
3550
|
-
permissionTier: "query-only"
|
|
2117
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
3551
2118
|
}),
|
|
3552
2119
|
ZCARD: tbl({
|
|
3553
2120
|
readOnly: true,
|
|
3554
2121
|
sizeGuard: { kind: "unbounded" },
|
|
3555
|
-
keyArity: { kind: "single", argIndex: 0 }
|
|
3556
|
-
permissionTier: "query-only"
|
|
2122
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
3557
2123
|
}),
|
|
3558
2124
|
ZSCORE: tbl({
|
|
3559
2125
|
readOnly: true,
|
|
3560
2126
|
sizeGuard: { kind: "unbounded" },
|
|
3561
|
-
keyArity: { kind: "single", argIndex: 0 }
|
|
3562
|
-
permissionTier: "query-only"
|
|
2127
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
3563
2128
|
}),
|
|
3564
2129
|
XLEN: tbl({
|
|
3565
2130
|
readOnly: true,
|
|
3566
2131
|
sizeGuard: { kind: "unbounded" },
|
|
3567
|
-
keyArity: { kind: "single", argIndex: 0 }
|
|
3568
|
-
permissionTier: "query-only"
|
|
2132
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
3569
2133
|
}),
|
|
3570
2134
|
SCAN: tbl({
|
|
3571
2135
|
readOnly: true,
|
|
3572
2136
|
sizeGuard: { kind: "rewrite-count", argIndex: 0 },
|
|
3573
|
-
keyArity: { kind: "
|
|
3574
|
-
permissionTier: "query-only"
|
|
2137
|
+
keyArity: { kind: "pattern-after-token", token: "MATCH" }
|
|
3575
2138
|
}),
|
|
3576
2139
|
HSCAN: tbl({
|
|
3577
2140
|
readOnly: true,
|
|
3578
2141
|
sizeGuard: { kind: "rewrite-count", argIndex: 1 },
|
|
3579
|
-
keyArity: { kind: "single", argIndex: 0 }
|
|
3580
|
-
permissionTier: "query-only"
|
|
2142
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
3581
2143
|
}),
|
|
3582
2144
|
SSCAN: tbl({
|
|
3583
2145
|
readOnly: true,
|
|
3584
2146
|
sizeGuard: { kind: "rewrite-count", argIndex: 1 },
|
|
3585
|
-
keyArity: { kind: "single", argIndex: 0 }
|
|
3586
|
-
permissionTier: "query-only"
|
|
2147
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
3587
2148
|
}),
|
|
3588
2149
|
ZSCAN: tbl({
|
|
3589
2150
|
readOnly: true,
|
|
3590
2151
|
sizeGuard: { kind: "rewrite-count", argIndex: 1 },
|
|
3591
|
-
keyArity: { kind: "single", argIndex: 0 }
|
|
3592
|
-
permissionTier: "query-only"
|
|
2152
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
3593
2153
|
}),
|
|
3594
2154
|
LRANGE: tbl({
|
|
3595
2155
|
readOnly: true,
|
|
3596
2156
|
sizeGuard: { kind: "rewrite-stop", argIndex: 2 },
|
|
3597
|
-
keyArity: { kind: "single", argIndex: 0 }
|
|
3598
|
-
permissionTier: "query-only"
|
|
2157
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
3599
2158
|
}),
|
|
3600
2159
|
ZRANGE: tbl({
|
|
3601
2160
|
readOnly: true,
|
|
3602
2161
|
sizeGuard: { kind: "rewrite-stop", argIndex: 2 },
|
|
3603
|
-
keyArity: { kind: "single", argIndex: 0 }
|
|
3604
|
-
permissionTier: "query-only"
|
|
2162
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
3605
2163
|
}),
|
|
3606
2164
|
ZREVRANGE: tbl({
|
|
3607
2165
|
readOnly: true,
|
|
3608
2166
|
sizeGuard: { kind: "rewrite-stop", argIndex: 2 },
|
|
3609
|
-
keyArity: { kind: "single", argIndex: 0 }
|
|
3610
|
-
permissionTier: "query-only"
|
|
2167
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
3611
2168
|
}),
|
|
3612
2169
|
ZRANGEBYSCORE: tbl({
|
|
3613
2170
|
readOnly: true,
|
|
3614
2171
|
sizeGuard: { kind: "rewrite-limit", argIndex: 3 },
|
|
3615
|
-
keyArity: { kind: "single", argIndex: 0 }
|
|
3616
|
-
permissionTier: "query-only"
|
|
2172
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
3617
2173
|
}),
|
|
3618
2174
|
HGETALL: tbl({
|
|
3619
2175
|
readOnly: true,
|
|
3620
2176
|
sizeGuard: { kind: "truncate" },
|
|
3621
|
-
keyArity: { kind: "single", argIndex: 0 }
|
|
3622
|
-
permissionTier: "query-only"
|
|
2177
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
3623
2178
|
}),
|
|
3624
2179
|
HKEYS: tbl({
|
|
3625
2180
|
readOnly: true,
|
|
3626
2181
|
sizeGuard: { kind: "truncate" },
|
|
3627
|
-
keyArity: { kind: "single", argIndex: 0 }
|
|
3628
|
-
permissionTier: "query-only"
|
|
2182
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
3629
2183
|
}),
|
|
3630
2184
|
HVALS: tbl({
|
|
3631
2185
|
readOnly: true,
|
|
3632
2186
|
sizeGuard: { kind: "truncate" },
|
|
3633
|
-
keyArity: { kind: "single", argIndex: 0 }
|
|
3634
|
-
permissionTier: "query-only"
|
|
2187
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
3635
2188
|
}),
|
|
3636
2189
|
SMEMBERS: tbl({
|
|
3637
2190
|
readOnly: true,
|
|
3638
2191
|
sizeGuard: { kind: "truncate" },
|
|
3639
|
-
keyArity: { kind: "single", argIndex: 0 }
|
|
3640
|
-
permissionTier: "query-only"
|
|
2192
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
3641
2193
|
}),
|
|
3642
2194
|
KEYS: tbl({
|
|
3643
2195
|
readOnly: true,
|
|
3644
2196
|
sizeGuard: { kind: "truncate" },
|
|
3645
|
-
keyArity: { kind: "pattern", argIndex: 0 }
|
|
3646
|
-
permissionTier: "query-only"
|
|
2197
|
+
keyArity: { kind: "pattern", argIndex: 0 }
|
|
3647
2198
|
}),
|
|
3648
2199
|
SET: tbl({
|
|
3649
2200
|
readOnly: false,
|
|
3650
2201
|
sizeGuard: { kind: "unbounded" },
|
|
3651
|
-
keyArity: { kind: "single", argIndex: 0 }
|
|
3652
|
-
permissionTier: "read-write"
|
|
2202
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
3653
2203
|
}),
|
|
3654
2204
|
SETEX: tbl({
|
|
3655
2205
|
readOnly: false,
|
|
3656
2206
|
sizeGuard: { kind: "unbounded" },
|
|
3657
|
-
keyArity: { kind: "single", argIndex: 0 }
|
|
3658
|
-
permissionTier: "read-write"
|
|
2207
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
3659
2208
|
}),
|
|
3660
2209
|
MSET: tbl({
|
|
3661
2210
|
readOnly: false,
|
|
3662
2211
|
sizeGuard: { kind: "unbounded" },
|
|
3663
|
-
keyArity: { kind: "multi-variable", startIndex: 0, step: 2 }
|
|
3664
|
-
permissionTier: "read-write"
|
|
2212
|
+
keyArity: { kind: "multi-variable", startIndex: 0, step: 2 }
|
|
3665
2213
|
}),
|
|
3666
2214
|
HSET: tbl({
|
|
3667
2215
|
readOnly: false,
|
|
3668
2216
|
sizeGuard: { kind: "unbounded" },
|
|
3669
|
-
keyArity: { kind: "single", argIndex: 0 }
|
|
3670
|
-
permissionTier: "read-write"
|
|
2217
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
3671
2218
|
}),
|
|
3672
2219
|
HMSET: tbl({
|
|
3673
2220
|
readOnly: false,
|
|
3674
2221
|
sizeGuard: { kind: "unbounded" },
|
|
3675
|
-
keyArity: { kind: "single", argIndex: 0 }
|
|
3676
|
-
permissionTier: "read-write"
|
|
2222
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
3677
2223
|
}),
|
|
3678
2224
|
HDEL: tbl({
|
|
3679
2225
|
readOnly: false,
|
|
3680
2226
|
sizeGuard: { kind: "unbounded" },
|
|
3681
|
-
keyArity: { kind: "single", argIndex: 0 }
|
|
3682
|
-
permissionTier: "read-write"
|
|
2227
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
3683
2228
|
}),
|
|
3684
2229
|
RPUSH: tbl({
|
|
3685
2230
|
readOnly: false,
|
|
3686
2231
|
sizeGuard: { kind: "unbounded" },
|
|
3687
|
-
keyArity: { kind: "single", argIndex: 0 }
|
|
3688
|
-
permissionTier: "read-write"
|
|
2232
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
3689
2233
|
}),
|
|
3690
2234
|
LPUSH: tbl({
|
|
3691
2235
|
readOnly: false,
|
|
3692
2236
|
sizeGuard: { kind: "unbounded" },
|
|
3693
|
-
keyArity: { kind: "single", argIndex: 0 }
|
|
3694
|
-
permissionTier: "read-write"
|
|
2237
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
3695
2238
|
}),
|
|
3696
2239
|
LREM: tbl({
|
|
3697
2240
|
readOnly: false,
|
|
3698
2241
|
sizeGuard: { kind: "unbounded" },
|
|
3699
|
-
keyArity: { kind: "single", argIndex: 0 }
|
|
3700
|
-
permissionTier: "read-write"
|
|
2242
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
3701
2243
|
}),
|
|
3702
2244
|
SADD: tbl({
|
|
3703
2245
|
readOnly: false,
|
|
3704
2246
|
sizeGuard: { kind: "unbounded" },
|
|
3705
|
-
keyArity: { kind: "single", argIndex: 0 }
|
|
3706
|
-
permissionTier: "read-write"
|
|
2247
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
3707
2248
|
}),
|
|
3708
2249
|
SREM: tbl({
|
|
3709
2250
|
readOnly: false,
|
|
3710
2251
|
sizeGuard: { kind: "unbounded" },
|
|
3711
|
-
keyArity: { kind: "single", argIndex: 0 }
|
|
3712
|
-
|
|
2252
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
2253
|
+
}),
|
|
2254
|
+
ZADD: tbl({
|
|
2255
|
+
readOnly: false,
|
|
2256
|
+
sizeGuard: { kind: "unbounded" },
|
|
2257
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
2258
|
+
}),
|
|
2259
|
+
ZREM: tbl({
|
|
2260
|
+
readOnly: false,
|
|
2261
|
+
sizeGuard: { kind: "unbounded" },
|
|
2262
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
2263
|
+
}),
|
|
2264
|
+
DEL: tbl({
|
|
2265
|
+
readOnly: false,
|
|
2266
|
+
sizeGuard: { kind: "unbounded" },
|
|
2267
|
+
keyArity: { kind: "multi-variable", startIndex: 0, step: 1 }
|
|
2268
|
+
}),
|
|
2269
|
+
UNLINK: tbl({
|
|
2270
|
+
readOnly: false,
|
|
2271
|
+
sizeGuard: { kind: "unbounded" },
|
|
2272
|
+
keyArity: { kind: "multi-variable", startIndex: 0, step: 1 }
|
|
2273
|
+
}),
|
|
2274
|
+
EXPIRE: tbl({
|
|
2275
|
+
readOnly: false,
|
|
2276
|
+
sizeGuard: { kind: "unbounded" },
|
|
2277
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
2278
|
+
}),
|
|
2279
|
+
PERSIST: tbl({
|
|
2280
|
+
readOnly: false,
|
|
2281
|
+
sizeGuard: { kind: "unbounded" },
|
|
2282
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
2283
|
+
}),
|
|
2284
|
+
RENAME: tbl({
|
|
2285
|
+
readOnly: false,
|
|
2286
|
+
sizeGuard: { kind: "unbounded" },
|
|
2287
|
+
keyArity: { kind: "multi-fixed", argIndices: [0, 1] }
|
|
2288
|
+
}),
|
|
2289
|
+
FLUSHDB: tbl({
|
|
2290
|
+
readOnly: false,
|
|
2291
|
+
sizeGuard: { kind: "reject", reason: "FLUSHDB destroys data" },
|
|
2292
|
+
keyArity: { kind: "no-key" }
|
|
2293
|
+
}),
|
|
2294
|
+
FLUSHALL: tbl({
|
|
2295
|
+
readOnly: false,
|
|
2296
|
+
sizeGuard: { kind: "reject", reason: "FLUSHALL destroys data" },
|
|
2297
|
+
keyArity: { kind: "no-key" }
|
|
2298
|
+
}),
|
|
2299
|
+
SHUTDOWN: tbl({
|
|
2300
|
+
readOnly: false,
|
|
2301
|
+
sizeGuard: { kind: "reject", reason: "SHUTDOWN stops the server" },
|
|
2302
|
+
keyArity: { kind: "no-key" }
|
|
2303
|
+
}),
|
|
2304
|
+
DEBUG: tbl({
|
|
2305
|
+
readOnly: false,
|
|
2306
|
+
sizeGuard: { kind: "reject", reason: "DEBUG is unsafe" },
|
|
2307
|
+
keyArity: { kind: "no-key" }
|
|
2308
|
+
}),
|
|
2309
|
+
PTTL: tbl({
|
|
2310
|
+
readOnly: true,
|
|
2311
|
+
sizeGuard: { kind: "unbounded" },
|
|
2312
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
2313
|
+
}),
|
|
2314
|
+
SETNX: tbl({
|
|
2315
|
+
readOnly: false,
|
|
2316
|
+
sizeGuard: { kind: "unbounded" },
|
|
2317
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
2318
|
+
}),
|
|
2319
|
+
PSETEX: tbl({
|
|
2320
|
+
readOnly: false,
|
|
2321
|
+
sizeGuard: { kind: "unbounded" },
|
|
2322
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
2323
|
+
}),
|
|
2324
|
+
APPEND: tbl({
|
|
2325
|
+
readOnly: false,
|
|
2326
|
+
sizeGuard: { kind: "unbounded" },
|
|
2327
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
2328
|
+
}),
|
|
2329
|
+
INCR: tbl({
|
|
2330
|
+
readOnly: false,
|
|
2331
|
+
sizeGuard: { kind: "unbounded" },
|
|
2332
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
2333
|
+
}),
|
|
2334
|
+
INCRBY: tbl({
|
|
2335
|
+
readOnly: false,
|
|
2336
|
+
sizeGuard: { kind: "unbounded" },
|
|
2337
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
2338
|
+
}),
|
|
2339
|
+
DECR: tbl({
|
|
2340
|
+
readOnly: false,
|
|
2341
|
+
sizeGuard: { kind: "unbounded" },
|
|
2342
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
2343
|
+
}),
|
|
2344
|
+
DECRBY: tbl({
|
|
2345
|
+
readOnly: false,
|
|
2346
|
+
sizeGuard: { kind: "unbounded" },
|
|
2347
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
2348
|
+
}),
|
|
2349
|
+
HSETNX: tbl({
|
|
2350
|
+
readOnly: false,
|
|
2351
|
+
sizeGuard: { kind: "unbounded" },
|
|
2352
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
2353
|
+
}),
|
|
2354
|
+
HINCRBY: tbl({
|
|
2355
|
+
readOnly: false,
|
|
2356
|
+
sizeGuard: { kind: "unbounded" },
|
|
2357
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
2358
|
+
}),
|
|
2359
|
+
LPOP: tbl({
|
|
2360
|
+
readOnly: false,
|
|
2361
|
+
sizeGuard: { kind: "unbounded" },
|
|
2362
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
2363
|
+
}),
|
|
2364
|
+
RPOP: tbl({
|
|
2365
|
+
readOnly: false,
|
|
2366
|
+
sizeGuard: { kind: "unbounded" },
|
|
2367
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
2368
|
+
}),
|
|
2369
|
+
LSET: tbl({
|
|
2370
|
+
readOnly: false,
|
|
2371
|
+
sizeGuard: { kind: "unbounded" },
|
|
2372
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
2373
|
+
}),
|
|
2374
|
+
XADD: tbl({
|
|
2375
|
+
readOnly: false,
|
|
2376
|
+
sizeGuard: { kind: "unbounded" },
|
|
2377
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
2378
|
+
}),
|
|
2379
|
+
XDEL: tbl({
|
|
2380
|
+
readOnly: false,
|
|
2381
|
+
sizeGuard: { kind: "unbounded" },
|
|
2382
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
2383
|
+
}),
|
|
2384
|
+
XRANGE: tbl({
|
|
2385
|
+
readOnly: true,
|
|
2386
|
+
sizeGuard: { kind: "unbounded" },
|
|
2387
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
2388
|
+
}),
|
|
2389
|
+
XREVRANGE: tbl({
|
|
2390
|
+
readOnly: true,
|
|
2391
|
+
sizeGuard: { kind: "unbounded" },
|
|
2392
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
2393
|
+
}),
|
|
2394
|
+
EXPIREAT: tbl({
|
|
2395
|
+
readOnly: false,
|
|
2396
|
+
sizeGuard: { kind: "unbounded" },
|
|
2397
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
2398
|
+
}),
|
|
2399
|
+
PEXPIRE: tbl({
|
|
2400
|
+
readOnly: false,
|
|
2401
|
+
sizeGuard: { kind: "unbounded" },
|
|
2402
|
+
keyArity: { kind: "single", argIndex: 0 }
|
|
2403
|
+
}),
|
|
2404
|
+
MSETNX: tbl({
|
|
2405
|
+
readOnly: false,
|
|
2406
|
+
sizeGuard: { kind: "unbounded" },
|
|
2407
|
+
keyArity: { kind: "multi-variable", startIndex: 0, step: 2 }
|
|
2408
|
+
}),
|
|
2409
|
+
XREAD: tbl({
|
|
2410
|
+
readOnly: true,
|
|
2411
|
+
sizeGuard: { kind: "unbounded" },
|
|
2412
|
+
keyArity: { kind: "after-token", token: "STREAMS" }
|
|
3713
2413
|
}),
|
|
3714
|
-
|
|
3715
|
-
readOnly:
|
|
2414
|
+
ECHO: tbl({
|
|
2415
|
+
readOnly: true,
|
|
3716
2416
|
sizeGuard: { kind: "unbounded" },
|
|
3717
|
-
keyArity: { kind: "
|
|
3718
|
-
permissionTier: "read-write"
|
|
2417
|
+
keyArity: { kind: "no-key" }
|
|
3719
2418
|
}),
|
|
3720
|
-
|
|
2419
|
+
CONFIG: tbl({
|
|
3721
2420
|
readOnly: false,
|
|
3722
2421
|
sizeGuard: { kind: "unbounded" },
|
|
3723
|
-
keyArity: { kind: "
|
|
3724
|
-
permissionTier: "read-write"
|
|
2422
|
+
keyArity: { kind: "no-key" }
|
|
3725
2423
|
}),
|
|
3726
|
-
|
|
2424
|
+
CLIENT: tbl({
|
|
3727
2425
|
readOnly: false,
|
|
3728
2426
|
sizeGuard: { kind: "unbounded" },
|
|
3729
|
-
keyArity: { kind: "
|
|
3730
|
-
permissionTier: "read-write"
|
|
2427
|
+
keyArity: { kind: "no-key" }
|
|
3731
2428
|
}),
|
|
3732
|
-
|
|
2429
|
+
MONITOR: tbl({
|
|
3733
2430
|
readOnly: false,
|
|
3734
2431
|
sizeGuard: { kind: "unbounded" },
|
|
3735
|
-
keyArity: { kind: "
|
|
3736
|
-
permissionTier: "read-write"
|
|
2432
|
+
keyArity: { kind: "no-key" }
|
|
3737
2433
|
}),
|
|
3738
|
-
|
|
2434
|
+
SAVE: tbl({
|
|
3739
2435
|
readOnly: false,
|
|
3740
2436
|
sizeGuard: { kind: "unbounded" },
|
|
3741
|
-
keyArity: { kind: "
|
|
3742
|
-
permissionTier: "read-write"
|
|
2437
|
+
keyArity: { kind: "no-key" }
|
|
3743
2438
|
}),
|
|
3744
|
-
|
|
2439
|
+
BGSAVE: tbl({
|
|
3745
2440
|
readOnly: false,
|
|
3746
2441
|
sizeGuard: { kind: "unbounded" },
|
|
3747
|
-
keyArity: { kind: "
|
|
3748
|
-
permissionTier: "read-write"
|
|
2442
|
+
keyArity: { kind: "no-key" }
|
|
3749
2443
|
}),
|
|
3750
|
-
|
|
2444
|
+
BGREWRITEAOF: tbl({
|
|
3751
2445
|
readOnly: false,
|
|
3752
2446
|
sizeGuard: { kind: "unbounded" },
|
|
3753
|
-
keyArity: { kind: "
|
|
3754
|
-
permissionTier: "read-write"
|
|
3755
|
-
}),
|
|
3756
|
-
FLUSHDB: tbl({
|
|
3757
|
-
readOnly: false,
|
|
3758
|
-
sizeGuard: { kind: "reject", reason: "FLUSHDB destroys data" },
|
|
3759
|
-
keyArity: { kind: "no-key" },
|
|
3760
|
-
permissionTier: "admin"
|
|
2447
|
+
keyArity: { kind: "no-key" }
|
|
3761
2448
|
}),
|
|
3762
|
-
|
|
2449
|
+
REPLICAOF: tbl({
|
|
3763
2450
|
readOnly: false,
|
|
3764
|
-
sizeGuard: { kind: "
|
|
3765
|
-
keyArity: { kind: "no-key" }
|
|
3766
|
-
permissionTier: "admin"
|
|
2451
|
+
sizeGuard: { kind: "unbounded" },
|
|
2452
|
+
keyArity: { kind: "no-key" }
|
|
3767
2453
|
}),
|
|
3768
|
-
|
|
2454
|
+
SLAVEOF: tbl({
|
|
3769
2455
|
readOnly: false,
|
|
3770
|
-
sizeGuard: { kind: "
|
|
3771
|
-
keyArity: { kind: "no-key" }
|
|
3772
|
-
permissionTier: "admin"
|
|
2456
|
+
sizeGuard: { kind: "unbounded" },
|
|
2457
|
+
keyArity: { kind: "no-key" }
|
|
3773
2458
|
}),
|
|
3774
|
-
|
|
2459
|
+
ACL: tbl({
|
|
3775
2460
|
readOnly: false,
|
|
3776
|
-
sizeGuard: { kind: "
|
|
3777
|
-
keyArity: { kind: "no-key" }
|
|
3778
|
-
permissionTier: "admin"
|
|
2461
|
+
sizeGuard: { kind: "unbounded" },
|
|
2462
|
+
keyArity: { kind: "no-key" }
|
|
3779
2463
|
}),
|
|
3780
2464
|
"CONFIG SET": tbl({
|
|
3781
2465
|
readOnly: false,
|
|
3782
2466
|
sizeGuard: { kind: "reject", reason: "CONFIG SET mutates server state" },
|
|
3783
|
-
keyArity: { kind: "no-key" }
|
|
3784
|
-
permissionTier: "admin"
|
|
2467
|
+
keyArity: { kind: "no-key" }
|
|
3785
2468
|
})
|
|
3786
2469
|
});
|
|
3787
|
-
function getCommandSpec(name) {
|
|
3788
|
-
return REDIS_COMMAND_TABLE[name.toUpperCase()];
|
|
3789
|
-
}
|
|
3790
|
-
|
|
3791
|
-
// src/adapters/redis/types.ts
|
|
3792
|
-
class BlacklistRejection extends Error {
|
|
3793
|
-
command;
|
|
3794
|
-
matchedKey;
|
|
3795
|
-
matchedPattern;
|
|
3796
|
-
constructor(message, command, matchedKey, matchedPattern) {
|
|
3797
|
-
super(message);
|
|
3798
|
-
this.command = command;
|
|
3799
|
-
this.matchedKey = matchedKey;
|
|
3800
|
-
this.matchedPattern = matchedPattern;
|
|
3801
|
-
this.name = "BlacklistRejection";
|
|
3802
|
-
Object.setPrototypeOf(this, BlacklistRejection.prototype);
|
|
3803
|
-
}
|
|
3804
|
-
}
|
|
3805
|
-
var REDIS_LIMIT_DEFAULT = 1000;
|
|
3806
|
-
|
|
3807
|
-
// src/adapters/redis/size-guard.ts
|
|
3808
|
-
function rewriteArgs(command, args, opts) {
|
|
3809
|
-
if (opts.noLimit)
|
|
3810
|
-
return { rewritten: args };
|
|
3811
|
-
const spec = getCommandSpec(command);
|
|
3812
|
-
if (!spec)
|
|
3813
|
-
return { rewritten: args };
|
|
3814
|
-
const original = args;
|
|
3815
|
-
switch (spec.sizeGuard.kind) {
|
|
3816
|
-
case "rewrite-count":
|
|
3817
|
-
return rewriteCount(command, args, original);
|
|
3818
|
-
case "rewrite-stop":
|
|
3819
|
-
return rewriteStop(command, args, spec.sizeGuard.argIndex, original);
|
|
3820
|
-
case "rewrite-limit":
|
|
3821
|
-
return rewriteLimit(command, args, original);
|
|
3822
|
-
default:
|
|
3823
|
-
return { rewritten: args };
|
|
3824
|
-
}
|
|
3825
|
-
}
|
|
3826
|
-
function rewriteCount(command, args, original) {
|
|
3827
|
-
const idx = args.findIndex((a) => a.toUpperCase() === "COUNT");
|
|
3828
|
-
if (idx === -1) {
|
|
3829
|
-
const rewritten = [...args, "COUNT", String(REDIS_LIMIT_DEFAULT)];
|
|
3830
|
-
return { rewritten, warning: { code: "REDIS_SIZE_REWRITE", command, original, rewritten } };
|
|
3831
|
-
}
|
|
3832
|
-
const n = parseInt(args[idx + 1] ?? "0", 10);
|
|
3833
|
-
if (n > REDIS_LIMIT_DEFAULT) {
|
|
3834
|
-
const rewritten = [...args];
|
|
3835
|
-
rewritten[idx + 1] = String(REDIS_LIMIT_DEFAULT);
|
|
3836
|
-
return { rewritten, warning: { code: "REDIS_SIZE_REWRITE", command, original, rewritten } };
|
|
3837
|
-
}
|
|
3838
|
-
return { rewritten: args };
|
|
3839
|
-
}
|
|
3840
|
-
function rewriteStop(command, args, stopIdx, original) {
|
|
3841
|
-
const startStr = args[stopIdx - 1] ?? "0";
|
|
3842
|
-
const stopStr = args[stopIdx];
|
|
3843
|
-
if (stopStr === undefined)
|
|
3844
|
-
return { rewritten: args };
|
|
3845
|
-
const start = parseInt(startStr, 10) || 0;
|
|
3846
|
-
const stop = parseInt(stopStr, 10);
|
|
3847
|
-
const span = stop === -1 ? Infinity : stop - start + 1;
|
|
3848
|
-
if (span > REDIS_LIMIT_DEFAULT) {
|
|
3849
|
-
const rewritten = [...args];
|
|
3850
|
-
rewritten[stopIdx] = String(start + REDIS_LIMIT_DEFAULT - 1);
|
|
3851
|
-
return { rewritten, warning: { code: "REDIS_SIZE_REWRITE", command, original, rewritten } };
|
|
3852
|
-
}
|
|
3853
|
-
return { rewritten: args };
|
|
3854
|
-
}
|
|
3855
|
-
function rewriteLimit(command, args, original) {
|
|
3856
|
-
const idx = args.findIndex((a) => a.toUpperCase() === "LIMIT");
|
|
3857
|
-
if (idx === -1) {
|
|
3858
|
-
const rewritten = [...args, "LIMIT", "0", String(REDIS_LIMIT_DEFAULT)];
|
|
3859
|
-
return { rewritten, warning: { code: "REDIS_SIZE_REWRITE", command, original, rewritten } };
|
|
3860
|
-
}
|
|
3861
|
-
const n = parseInt(args[idx + 2] ?? "0", 10);
|
|
3862
|
-
if (n > REDIS_LIMIT_DEFAULT) {
|
|
3863
|
-
const rewritten = [...args];
|
|
3864
|
-
rewritten[idx + 2] = String(REDIS_LIMIT_DEFAULT);
|
|
3865
|
-
return { rewritten, warning: { code: "REDIS_SIZE_REWRITE", command, original, rewritten } };
|
|
3866
|
-
}
|
|
3867
|
-
return { rewritten: args };
|
|
3868
|
-
}
|
|
3869
|
-
function truncateResult(command, reply, opts) {
|
|
3870
|
-
if (opts.noLimit)
|
|
3871
|
-
return { value: reply };
|
|
3872
|
-
const spec = getCommandSpec(command);
|
|
3873
|
-
if (!spec || spec.sizeGuard.kind !== "truncate")
|
|
3874
|
-
return { value: reply };
|
|
3875
|
-
if (Array.isArray(reply)) {
|
|
3876
|
-
if (reply.length > REDIS_LIMIT_DEFAULT) {
|
|
3877
|
-
const droppedAtLeast = reply.length - REDIS_LIMIT_DEFAULT;
|
|
3878
|
-
const value = reply.slice(0, REDIS_LIMIT_DEFAULT);
|
|
3879
|
-
return {
|
|
3880
|
-
value,
|
|
3881
|
-
warning: {
|
|
3882
|
-
code: "REDIS_SIZE_TRUNCATE",
|
|
3883
|
-
command,
|
|
3884
|
-
kept: REDIS_LIMIT_DEFAULT,
|
|
3885
|
-
droppedAtLeast
|
|
3886
|
-
}
|
|
3887
|
-
};
|
|
3888
|
-
}
|
|
3889
|
-
return { value: reply };
|
|
3890
|
-
}
|
|
3891
|
-
if (reply && typeof reply === "object") {
|
|
3892
|
-
const obj = reply;
|
|
3893
|
-
const keys = Object.keys(obj);
|
|
3894
|
-
if (keys.length > REDIS_LIMIT_DEFAULT) {
|
|
3895
|
-
const dropped = keys.length - REDIS_LIMIT_DEFAULT;
|
|
3896
|
-
const truncated = {};
|
|
3897
|
-
for (let i = 0;i < REDIS_LIMIT_DEFAULT; i++)
|
|
3898
|
-
truncated[keys[i]] = obj[keys[i]];
|
|
3899
|
-
return {
|
|
3900
|
-
value: truncated,
|
|
3901
|
-
warning: {
|
|
3902
|
-
code: "REDIS_SIZE_TRUNCATE",
|
|
3903
|
-
command,
|
|
3904
|
-
kept: REDIS_LIMIT_DEFAULT,
|
|
3905
|
-
droppedAtLeast: dropped
|
|
3906
|
-
}
|
|
3907
|
-
};
|
|
3908
|
-
}
|
|
3909
|
-
}
|
|
3910
|
-
return { value: reply };
|
|
3911
|
-
}
|
|
3912
2470
|
|
|
3913
2471
|
// src/utils/glob.ts
|
|
3914
2472
|
function globToRegex(glob) {
|
|
@@ -3961,968 +2519,11 @@ function isValidCharacterClass(body) {
|
|
|
3961
2519
|
}
|
|
3962
2520
|
}
|
|
3963
2521
|
|
|
3964
|
-
// src/adapters/redis/
|
|
3965
|
-
|
|
3966
|
-
const ra = globToRegex(a);
|
|
3967
|
-
const rb = globToRegex(b);
|
|
3968
|
-
const probeA = sampleFromGlob(a);
|
|
3969
|
-
const probeB = sampleFromGlob(b);
|
|
3970
|
-
if (probeA !== null && rb.test(probeA))
|
|
3971
|
-
return true;
|
|
3972
|
-
if (probeB !== null && ra.test(probeB))
|
|
3973
|
-
return true;
|
|
3974
|
-
return false;
|
|
3975
|
-
}
|
|
3976
|
-
function sampleFromGlob(glob) {
|
|
3977
|
-
let s = "";
|
|
3978
|
-
for (let i = 0;i < glob.length; i++) {
|
|
3979
|
-
const c = glob[i];
|
|
3980
|
-
if (c === "*")
|
|
3981
|
-
s += "__star__";
|
|
3982
|
-
else if (c === "?")
|
|
3983
|
-
s += "x";
|
|
3984
|
-
else if (c === "[") {
|
|
3985
|
-
const end = glob.indexOf("]", i);
|
|
3986
|
-
if (end === -1)
|
|
3987
|
-
return null;
|
|
3988
|
-
const first = glob[i + 1];
|
|
3989
|
-
if (!first)
|
|
3990
|
-
return null;
|
|
3991
|
-
s += first;
|
|
3992
|
-
i = end;
|
|
3993
|
-
} else
|
|
3994
|
-
s += c;
|
|
3995
|
-
}
|
|
3996
|
-
return s;
|
|
3997
|
-
}
|
|
3998
|
-
function checkKeyArgs(command, args, rules) {
|
|
3999
|
-
if (rules.length === 0)
|
|
4000
|
-
return { ok: true };
|
|
4001
|
-
const spec = getCommandSpec(command);
|
|
4002
|
-
if (!spec)
|
|
4003
|
-
return { ok: true };
|
|
4004
|
-
const keyIndexes = expandKeyArity(spec.keyArity, args.length);
|
|
4005
|
-
for (const idx of keyIndexes) {
|
|
4006
|
-
const key = args[idx];
|
|
4007
|
-
if (key === undefined)
|
|
4008
|
-
continue;
|
|
4009
|
-
for (const pat of rules) {
|
|
4010
|
-
if (globToRegex(pat).test(key)) {
|
|
4011
|
-
return { ok: false, matchedKey: key, matchedPattern: pat };
|
|
4012
|
-
}
|
|
4013
|
-
}
|
|
4014
|
-
}
|
|
4015
|
-
if (spec.keyArity.kind === "pattern") {
|
|
4016
|
-
const userPat = args[spec.keyArity.argIndex];
|
|
4017
|
-
if (userPat !== undefined) {
|
|
4018
|
-
for (const pat of rules) {
|
|
4019
|
-
if (patternsOverlap(userPat, pat)) {
|
|
4020
|
-
return { ok: false, matchedKey: null, matchedPattern: pat };
|
|
4021
|
-
}
|
|
4022
|
-
}
|
|
4023
|
-
}
|
|
4024
|
-
}
|
|
4025
|
-
return { ok: true };
|
|
4026
|
-
}
|
|
4027
|
-
function expandKeyArity(arity, argCount) {
|
|
4028
|
-
switch (arity.kind) {
|
|
4029
|
-
case "no-key":
|
|
4030
|
-
case "pattern":
|
|
4031
|
-
return [];
|
|
4032
|
-
case "single":
|
|
4033
|
-
return [arity.argIndex];
|
|
4034
|
-
case "multi-fixed":
|
|
4035
|
-
return arity.argIndices;
|
|
4036
|
-
case "multi-variable": {
|
|
4037
|
-
const indices = [];
|
|
4038
|
-
for (let i = arity.startIndex;i < argCount; i += arity.step)
|
|
4039
|
-
indices.push(i);
|
|
4040
|
-
return indices;
|
|
4041
|
-
}
|
|
4042
|
-
}
|
|
4043
|
-
}
|
|
2522
|
+
// src/adapters/redis/returned-key-names.ts
|
|
2523
|
+
var RETURNS_KEY_NAMES = new Set(["SCAN", "KEYS"]);
|
|
4044
2524
|
|
|
4045
2525
|
// src/adapters/redis/value-masker.ts
|
|
4046
|
-
var REDACTED = "[REDACTED]";
|
|
4047
2526
|
var MASKABLE = new Set(["GET", "GETRANGE", "HGETALL", "HGET", "HMGET", "HVALS"]);
|
|
4048
|
-
function planFor(key, rules) {
|
|
4049
|
-
const matched = rules.filter((r) => globToRegex(r.keyPattern).test(key));
|
|
4050
|
-
if (matched.length === 0)
|
|
4051
|
-
return null;
|
|
4052
|
-
const wholeValue = matched.some((r) => !r.fields || r.fields.length === 0);
|
|
4053
|
-
const fields = new Set(matched.flatMap((r) => r.fields ?? []));
|
|
4054
|
-
return { wholeValue, fields };
|
|
4055
|
-
}
|
|
4056
|
-
function maskRedisRows(command, args, rows, rules) {
|
|
4057
|
-
if (rules.length === 0)
|
|
4058
|
-
return rows;
|
|
4059
|
-
const head = command.toUpperCase();
|
|
4060
|
-
if (!MASKABLE.has(head))
|
|
4061
|
-
return rows;
|
|
4062
|
-
const key = args[0];
|
|
4063
|
-
if (key === undefined)
|
|
4064
|
-
return rows;
|
|
4065
|
-
const plan = planFor(key, rules);
|
|
4066
|
-
if (!plan)
|
|
4067
|
-
return rows;
|
|
4068
|
-
if (head === "GET" || head === "GETRANGE") {
|
|
4069
|
-
if (!plan.wholeValue)
|
|
4070
|
-
return rows;
|
|
4071
|
-
return rows.map((r) => ({ ...r, value: REDACTED }));
|
|
4072
|
-
}
|
|
4073
|
-
if (head === "HGETALL") {
|
|
4074
|
-
return rows.map((row) => {
|
|
4075
|
-
const out = {};
|
|
4076
|
-
for (const [field, value] of Object.entries(row)) {
|
|
4077
|
-
out[field] = plan.wholeValue || plan.fields.has(field) ? REDACTED : value;
|
|
4078
|
-
}
|
|
4079
|
-
return out;
|
|
4080
|
-
});
|
|
4081
|
-
}
|
|
4082
|
-
if (head === "HGET") {
|
|
4083
|
-
const field = args[1];
|
|
4084
|
-
const hit = plan.wholeValue || field !== undefined && plan.fields.has(field);
|
|
4085
|
-
return hit ? rows.map((r) => ({ ...r, value: REDACTED })) : rows;
|
|
4086
|
-
}
|
|
4087
|
-
if (head === "HMGET") {
|
|
4088
|
-
return rows.map((row) => {
|
|
4089
|
-
const idx = typeof row.index === "number" ? row.index : Number(row.index);
|
|
4090
|
-
const field = args[1 + idx];
|
|
4091
|
-
const hit = plan.wholeValue || field !== undefined && plan.fields.has(field);
|
|
4092
|
-
return hit ? { ...row, value: REDACTED } : row;
|
|
4093
|
-
});
|
|
4094
|
-
}
|
|
4095
|
-
if (head === "HVALS") {
|
|
4096
|
-
if (!plan.wholeValue)
|
|
4097
|
-
return rows;
|
|
4098
|
-
return rows.map((r) => ({ ...r, value: REDACTED }));
|
|
4099
|
-
}
|
|
4100
|
-
return rows;
|
|
4101
|
-
}
|
|
4102
|
-
|
|
4103
|
-
// src/adapters/redis-adapter.ts
|
|
4104
|
-
var REDIS_LIST_KEY_LIMIT = 1000;
|
|
4105
|
-
|
|
4106
|
-
class RedisAdapter {
|
|
4107
|
-
options;
|
|
4108
|
-
ClientClass;
|
|
4109
|
-
client = null;
|
|
4110
|
-
blacklistRules = [];
|
|
4111
|
-
constructor(options, ClientClass = null) {
|
|
4112
|
-
this.options = options;
|
|
4113
|
-
this.ClientClass = ClientClass;
|
|
4114
|
-
if (options.port < 1 || options.port > 65535) {
|
|
4115
|
-
throw new Error(`Invalid port number: ${options.port}`);
|
|
4116
|
-
}
|
|
4117
|
-
}
|
|
4118
|
-
setBlacklistRules(rules) {
|
|
4119
|
-
this.blacklistRules = rules;
|
|
4120
|
-
}
|
|
4121
|
-
maskRules = [];
|
|
4122
|
-
setMaskRules(rules) {
|
|
4123
|
-
this.maskRules = rules;
|
|
4124
|
-
}
|
|
4125
|
-
async resolveClientClass() {
|
|
4126
|
-
if (this.ClientClass)
|
|
4127
|
-
return this.ClientClass;
|
|
4128
|
-
const mod = await Promise.resolve(globalThis.Bun);
|
|
4129
|
-
return mod.RedisClient;
|
|
4130
|
-
}
|
|
4131
|
-
requireClient() {
|
|
4132
|
-
if (!this.client) {
|
|
4133
|
-
throw new ConnectionError("UNKNOWN", "\u5C1A\u672A\u9023\u7DDA\uFF0C\u8ACB\u5148\u547C\u53EB connect()", ["\u8ACB\u5148\u547C\u53EB connect()"]);
|
|
4134
|
-
}
|
|
4135
|
-
return this.client;
|
|
4136
|
-
}
|
|
4137
|
-
async connect() {
|
|
4138
|
-
const ClientClass = await this.resolveClientClass();
|
|
4139
|
-
const url = buildRedisUrl(this.options);
|
|
4140
|
-
try {
|
|
4141
|
-
this.client = new ClientClass(url, {
|
|
4142
|
-
connectionTimeout: this.options.timeout ?? 5000,
|
|
4143
|
-
autoReconnect: false,
|
|
4144
|
-
maxRetries: 0,
|
|
4145
|
-
enableOfflineQueue: false
|
|
4146
|
-
});
|
|
4147
|
-
try {
|
|
4148
|
-
this.client.onclose = () => {};
|
|
4149
|
-
} catch {}
|
|
4150
|
-
await this.client.connect();
|
|
4151
|
-
} catch (err) {
|
|
4152
|
-
try {
|
|
4153
|
-
this.client?.close?.();
|
|
4154
|
-
} catch {}
|
|
4155
|
-
this.client = null;
|
|
4156
|
-
const message = err.message ?? "Unknown error";
|
|
4157
|
-
const code = inferConnectionCode(message, err.code);
|
|
4158
|
-
throw new ConnectionError(code, `Redis \u9023\u7DDA\u5931\u6557: ${message}`, [
|
|
4159
|
-
"\u8ACB\u78BA\u8A8D Redis \u670D\u52D9\u6B63\u5728\u57F7\u884C\uFF08\u4F8B\u5982\uFF1Aredis-cli ping\uFF09",
|
|
4160
|
-
`\u8ACB\u78BA\u8A8D host/port \u6B63\u78BA\uFF1A${this.options.host}:${this.options.port}`,
|
|
4161
|
-
"\u82E5\u6709\u8A2D\u5BC6\u78BC\u8ACB\u78BA\u8A8D password \u8207 ACL \u8A2D\u5B9A\u4E00\u81F4"
|
|
4162
|
-
]);
|
|
4163
|
-
}
|
|
4164
|
-
}
|
|
4165
|
-
async disconnect() {
|
|
4166
|
-
if (!this.client)
|
|
4167
|
-
return;
|
|
4168
|
-
try {
|
|
4169
|
-
this.client.close();
|
|
4170
|
-
} catch {} finally {
|
|
4171
|
-
this.client = null;
|
|
4172
|
-
}
|
|
4173
|
-
}
|
|
4174
|
-
async testConnection() {
|
|
4175
|
-
const client = this.requireClient();
|
|
4176
|
-
const reply = await client.send("PING", []);
|
|
4177
|
-
return reply === "PONG";
|
|
4178
|
-
}
|
|
4179
|
-
async getServerVersion() {
|
|
4180
|
-
const client = this.requireClient();
|
|
4181
|
-
const info = await client.send("INFO", ["server"]);
|
|
4182
|
-
const match = info.match(/^redis_version:(.+)$/m);
|
|
4183
|
-
return match?.[1]?.trim() ?? "unknown";
|
|
4184
|
-
}
|
|
4185
|
-
async listCollections(options) {
|
|
4186
|
-
const { names } = await this.sampleKeyNames(options?.limit ?? REDIS_LIST_KEY_LIMIT);
|
|
4187
|
-
return names.map((name) => ({ name }));
|
|
4188
|
-
}
|
|
4189
|
-
async sampleKeyNames(limit) {
|
|
4190
|
-
const client = this.requireClient();
|
|
4191
|
-
const rules = this.blacklistRules;
|
|
4192
|
-
const regexes = rules.map((p) => globToRegex(p));
|
|
4193
|
-
const { keys, scanned } = await scanAllKeys(client, "*", limit, limit, (key) => regexes.every((r) => !r.test(key)));
|
|
4194
|
-
return { names: keys, truncated: scanned >= limit };
|
|
4195
|
-
}
|
|
4196
|
-
async getDbSize() {
|
|
4197
|
-
const client = this.requireClient();
|
|
4198
|
-
const reply = await client.send("DBSIZE", []);
|
|
4199
|
-
return reply;
|
|
4200
|
-
}
|
|
4201
|
-
async listTables() {
|
|
4202
|
-
const collections = await this.listCollections();
|
|
4203
|
-
return collections.map((c) => ({ name: c.name, columns: [] }));
|
|
4204
|
-
}
|
|
4205
|
-
async getTableSchema(keyName) {
|
|
4206
|
-
const blk = checkKeyArgs("GET", [keyName], this.blacklistRules);
|
|
4207
|
-
if (!blk.ok) {
|
|
4208
|
-
throw new BlacklistRejection(`BlacklistRejection: schema lookup on key '${keyName}' rejected
|
|
4209
|
-
matched blacklist pattern: ${blk.matchedPattern}`, "SCHEMA", keyName, blk.matchedPattern);
|
|
4210
|
-
}
|
|
4211
|
-
const client = this.requireClient();
|
|
4212
|
-
const type = await client.send("TYPE", [keyName]);
|
|
4213
|
-
if (type === "none") {
|
|
4214
|
-
return { name: keyName, columns: [], tableType: "table" };
|
|
4215
|
-
}
|
|
4216
|
-
const ttl = await client.ttl(keyName);
|
|
4217
|
-
const sizeInfo = await readSizeInfo(client, keyName, type);
|
|
4218
|
-
const columns = [
|
|
4219
|
-
{ name: "type", type, nullable: false },
|
|
4220
|
-
{
|
|
4221
|
-
name: "ttl",
|
|
4222
|
-
type: ttl >= 0 ? `${ttl}s` : ttl === -1 ? "no expiry" : "missing",
|
|
4223
|
-
nullable: false
|
|
4224
|
-
},
|
|
4225
|
-
{ name: "size", type: String(sizeInfo.size), nullable: false }
|
|
4226
|
-
];
|
|
4227
|
-
if (sizeInfo.sample) {
|
|
4228
|
-
columns.push({ name: "sample", type: sizeInfo.sample, nullable: true });
|
|
4229
|
-
}
|
|
4230
|
-
return {
|
|
4231
|
-
name: keyName,
|
|
4232
|
-
columns,
|
|
4233
|
-
tableType: "table",
|
|
4234
|
-
estimatedRowCount: sizeInfo.size
|
|
4235
|
-
};
|
|
4236
|
-
}
|
|
4237
|
-
async dispatchCommand(head, rest) {
|
|
4238
|
-
const client = this.requireClient();
|
|
4239
|
-
const reply = await client.send(head, rest);
|
|
4240
|
-
return wrapReply(head, reply);
|
|
4241
|
-
}
|
|
4242
|
-
async execute(command, _params, options) {
|
|
4243
|
-
const tokens = parseRedisCommand(command);
|
|
4244
|
-
if (tokens.length === 0) {
|
|
4245
|
-
throw new Error("Redis \u6307\u4EE4\u4E0D\u53EF\u70BA\u7A7A");
|
|
4246
|
-
}
|
|
4247
|
-
const head = String(tokens[0]).toUpperCase();
|
|
4248
|
-
const rest = tokens.slice(1).map(String);
|
|
4249
|
-
const blk = checkKeyArgs(head, rest, this.blacklistRules);
|
|
4250
|
-
if (!blk.ok) {
|
|
4251
|
-
const msg = blk.matchedKey ? `BlacklistRejection: command ${head} on key '${blk.matchedKey}' rejected
|
|
4252
|
-
matched blacklist pattern: ${blk.matchedPattern}` : `BlacklistRejection: ${head} pattern overlaps blacklist pattern: ${blk.matchedPattern}`;
|
|
4253
|
-
throw new BlacklistRejection(msg, head, blk.matchedKey ?? null, blk.matchedPattern);
|
|
4254
|
-
}
|
|
4255
|
-
const noLimit = options?.noLimit === true;
|
|
4256
|
-
const rw = rewriteArgs(head, rest, { noLimit });
|
|
4257
|
-
const warnings = [];
|
|
4258
|
-
if (rw.warning)
|
|
4259
|
-
warnings.push(rw.warning);
|
|
4260
|
-
const spec = getCommandSpec(head);
|
|
4261
|
-
if (spec?.sizeGuard.kind === "truncate") {
|
|
4262
|
-
const client = this.requireClient();
|
|
4263
|
-
const rawReply = await client.send(head, rw.rewritten);
|
|
4264
|
-
const tr = truncateResult(head, rawReply, { noLimit });
|
|
4265
|
-
if (tr.warning)
|
|
4266
|
-
warnings.push(tr.warning);
|
|
4267
|
-
const rows2 = wrapReply(head, tr.value);
|
|
4268
|
-
const masked2 = maskRedisRows(head, rest, rows2, this.maskRules);
|
|
4269
|
-
return finishResult(masked2, warnings);
|
|
4270
|
-
}
|
|
4271
|
-
const rows = await this.dispatchCommand(head, rw.rewritten);
|
|
4272
|
-
const masked = maskRedisRows(head, rest, rows, this.maskRules);
|
|
4273
|
-
return finishResult(masked, warnings);
|
|
4274
|
-
}
|
|
4275
|
-
async insert(keyName, data) {
|
|
4276
|
-
const client = this.requireClient();
|
|
4277
|
-
const type = data.__type ?? "string";
|
|
4278
|
-
if (type === "string") {
|
|
4279
|
-
const value = String(data.value ?? "");
|
|
4280
|
-
await client.set(keyName, value);
|
|
4281
|
-
if (typeof data.ttl === "number" && data.ttl > 0) {
|
|
4282
|
-
await client.expire(keyName, data.ttl);
|
|
4283
|
-
}
|
|
4284
|
-
return { rows: [], affectedRows: 1 };
|
|
4285
|
-
}
|
|
4286
|
-
if (type === "hash") {
|
|
4287
|
-
const fields = data.fields;
|
|
4288
|
-
if (!fields)
|
|
4289
|
-
throw new Error("hash insert \u9700\u8981 fields \u7269\u4EF6");
|
|
4290
|
-
const flat = [];
|
|
4291
|
-
for (const [k, v] of Object.entries(fields))
|
|
4292
|
-
flat.push(k, String(v));
|
|
4293
|
-
await client.hmset(keyName, flat);
|
|
4294
|
-
return { rows: [], affectedRows: Object.keys(fields).length };
|
|
4295
|
-
}
|
|
4296
|
-
if (type === "list") {
|
|
4297
|
-
const values = data.values;
|
|
4298
|
-
if (!values || !Array.isArray(values))
|
|
4299
|
-
throw new Error("list insert \u9700\u8981 values \u9663\u5217");
|
|
4300
|
-
await client.send("RPUSH", [keyName, ...values.map(String)]);
|
|
4301
|
-
return { rows: [], affectedRows: values.length };
|
|
4302
|
-
}
|
|
4303
|
-
if (type === "set") {
|
|
4304
|
-
const values = data.values;
|
|
4305
|
-
if (!values || !Array.isArray(values))
|
|
4306
|
-
throw new Error("set insert \u9700\u8981 values \u9663\u5217");
|
|
4307
|
-
await client.send("SADD", [keyName, ...values.map(String)]);
|
|
4308
|
-
return { rows: [], affectedRows: values.length };
|
|
4309
|
-
}
|
|
4310
|
-
if (type === "zset") {
|
|
4311
|
-
const members = data.members;
|
|
4312
|
-
if (!members)
|
|
4313
|
-
throw new Error("zset insert \u9700\u8981 members \u7269\u4EF6 (member -> score)");
|
|
4314
|
-
const flat = [];
|
|
4315
|
-
for (const [m, s] of Object.entries(members))
|
|
4316
|
-
flat.push(String(s), m);
|
|
4317
|
-
await client.send("ZADD", [keyName, ...flat]);
|
|
4318
|
-
return { rows: [], affectedRows: Object.keys(members).length };
|
|
4319
|
-
}
|
|
4320
|
-
throw new Error(`\u4E0D\u652F\u63F4\u7684 insert \u985E\u578B: ${String(type)}`);
|
|
4321
|
-
}
|
|
4322
|
-
async update(keyName, _filter, update) {
|
|
4323
|
-
const client = this.requireClient();
|
|
4324
|
-
const fields = update.fields;
|
|
4325
|
-
if (fields) {
|
|
4326
|
-
const flat = [];
|
|
4327
|
-
for (const [k, v] of Object.entries(fields))
|
|
4328
|
-
flat.push(k, String(v));
|
|
4329
|
-
await client.hmset(keyName, flat);
|
|
4330
|
-
return { rows: [], affectedRows: Object.keys(fields).length };
|
|
4331
|
-
}
|
|
4332
|
-
const type = update.__type;
|
|
4333
|
-
if (type === "list") {
|
|
4334
|
-
const values = update.values;
|
|
4335
|
-
await client.send("RPUSH", [keyName, ...values.map(String)]);
|
|
4336
|
-
return { rows: [], affectedRows: values.length };
|
|
4337
|
-
}
|
|
4338
|
-
if (type === "set") {
|
|
4339
|
-
const values = update.values;
|
|
4340
|
-
await client.send("SADD", [keyName, ...values.map(String)]);
|
|
4341
|
-
return { rows: [], affectedRows: values.length };
|
|
4342
|
-
}
|
|
4343
|
-
if (type === "zset") {
|
|
4344
|
-
const members = update.members;
|
|
4345
|
-
const flat = [];
|
|
4346
|
-
for (const [m, s] of Object.entries(members))
|
|
4347
|
-
flat.push(String(s), m);
|
|
4348
|
-
await client.send("ZADD", [keyName, ...flat]);
|
|
4349
|
-
return { rows: [], affectedRows: Object.keys(members).length };
|
|
4350
|
-
}
|
|
4351
|
-
if ("value" in update) {
|
|
4352
|
-
await client.set(keyName, String(update.value));
|
|
4353
|
-
return { rows: [], affectedRows: 1 };
|
|
4354
|
-
}
|
|
4355
|
-
throw new Error("update \u9700\u8981 fields, values, members \u6216 value");
|
|
4356
|
-
}
|
|
4357
|
-
async delete(keyName, filter) {
|
|
4358
|
-
const client = this.requireClient();
|
|
4359
|
-
const field = filter.field;
|
|
4360
|
-
if (field) {
|
|
4361
|
-
const removed2 = await client.send("HDEL", [keyName, field]);
|
|
4362
|
-
return { rows: [], affectedRows: removed2 };
|
|
4363
|
-
}
|
|
4364
|
-
const value = filter.value;
|
|
4365
|
-
if (value) {
|
|
4366
|
-
const type = await client.send("TYPE", [keyName]);
|
|
4367
|
-
let removed2 = 0;
|
|
4368
|
-
if (type === "list") {
|
|
4369
|
-
removed2 = await client.send("LREM", [keyName, "0", value]);
|
|
4370
|
-
} else if (type === "set") {
|
|
4371
|
-
removed2 = await client.send("SREM", [keyName, value]);
|
|
4372
|
-
} else if (type === "zset") {
|
|
4373
|
-
removed2 = await client.send("ZREM", [keyName, value]);
|
|
4374
|
-
}
|
|
4375
|
-
return { rows: [], affectedRows: removed2 };
|
|
4376
|
-
}
|
|
4377
|
-
const removed = await client.del(keyName);
|
|
4378
|
-
return { rows: [], affectedRows: removed };
|
|
4379
|
-
}
|
|
4380
|
-
}
|
|
4381
|
-
function finishResult(rows, warnings) {
|
|
4382
|
-
const columnNames = rows[0] ? Object.keys(rows[0]) : ["value"];
|
|
4383
|
-
return {
|
|
4384
|
-
rows,
|
|
4385
|
-
affectedRows: rows.length,
|
|
4386
|
-
rowCount: rows.length,
|
|
4387
|
-
columnNames,
|
|
4388
|
-
...warnings.length > 0 ? { warnings } : {}
|
|
4389
|
-
};
|
|
4390
|
-
}
|
|
4391
|
-
function buildRedisUrl(opts) {
|
|
4392
|
-
let auth = "";
|
|
4393
|
-
if (opts.user || opts.password) {
|
|
4394
|
-
const u = encodeURIComponent(opts.user || "");
|
|
4395
|
-
const p = encodeURIComponent(opts.password || "");
|
|
4396
|
-
auth = `${u}:${p}@`;
|
|
4397
|
-
}
|
|
4398
|
-
const dbPart = opts.database ? `/${opts.database}` : "";
|
|
4399
|
-
return `redis://${auth}${opts.host}:${opts.port}${dbPart}`;
|
|
4400
|
-
}
|
|
4401
|
-
function inferConnectionCode(message, driverCode) {
|
|
4402
|
-
const m = message.toLowerCase();
|
|
4403
|
-
if (driverCode === "ERR_REDIS_AUTHENTICATION_FAILED")
|
|
4404
|
-
return "AUTH_FAILED";
|
|
4405
|
-
if (driverCode === "ERR_REDIS_CONNECTION_CLOSED")
|
|
4406
|
-
return "ECONNREFUSED";
|
|
4407
|
-
if (driverCode === "ECONNREFUSED" || m.includes("econnrefused") || m.includes("refused")) {
|
|
4408
|
-
return "ECONNREFUSED";
|
|
4409
|
-
}
|
|
4410
|
-
if (driverCode === "ETIMEDOUT" || m.includes("timeout") || m.includes("timed out")) {
|
|
4411
|
-
return "ETIMEDOUT";
|
|
4412
|
-
}
|
|
4413
|
-
if (driverCode === "ENOTFOUND" || m.includes("enotfound") || m.includes("getaddrinfo")) {
|
|
4414
|
-
return "ENOTFOUND";
|
|
4415
|
-
}
|
|
4416
|
-
if (m.includes("noauth") || m.includes("wrongpass") || m.includes("auth")) {
|
|
4417
|
-
return "AUTH_FAILED";
|
|
4418
|
-
}
|
|
4419
|
-
return "UNKNOWN";
|
|
4420
|
-
}
|
|
4421
|
-
function parseRedisCommand(input) {
|
|
4422
|
-
const tokens = [];
|
|
4423
|
-
let buf = "";
|
|
4424
|
-
let quote = null;
|
|
4425
|
-
let i = 0;
|
|
4426
|
-
let inToken = false;
|
|
4427
|
-
const push = () => {
|
|
4428
|
-
tokens.push(buf);
|
|
4429
|
-
buf = "";
|
|
4430
|
-
inToken = false;
|
|
4431
|
-
};
|
|
4432
|
-
while (i < input.length) {
|
|
4433
|
-
const ch = input[i];
|
|
4434
|
-
if (quote) {
|
|
4435
|
-
if (ch === "\\" && i + 1 < input.length) {
|
|
4436
|
-
buf += input[i + 1];
|
|
4437
|
-
i += 2;
|
|
4438
|
-
continue;
|
|
4439
|
-
}
|
|
4440
|
-
if (ch === quote) {
|
|
4441
|
-
quote = null;
|
|
4442
|
-
i++;
|
|
4443
|
-
continue;
|
|
4444
|
-
}
|
|
4445
|
-
buf += ch;
|
|
4446
|
-
i++;
|
|
4447
|
-
continue;
|
|
4448
|
-
}
|
|
4449
|
-
if (ch === '"' || ch === "'") {
|
|
4450
|
-
quote = ch;
|
|
4451
|
-
inToken = true;
|
|
4452
|
-
i++;
|
|
4453
|
-
continue;
|
|
4454
|
-
}
|
|
4455
|
-
if (/\s/.test(ch)) {
|
|
4456
|
-
if (inToken)
|
|
4457
|
-
push();
|
|
4458
|
-
i++;
|
|
4459
|
-
continue;
|
|
4460
|
-
}
|
|
4461
|
-
if (ch === "\\" && i + 1 < input.length) {
|
|
4462
|
-
buf += input[i + 1];
|
|
4463
|
-
i += 2;
|
|
4464
|
-
inToken = true;
|
|
4465
|
-
continue;
|
|
4466
|
-
}
|
|
4467
|
-
buf += ch;
|
|
4468
|
-
inToken = true;
|
|
4469
|
-
i++;
|
|
4470
|
-
}
|
|
4471
|
-
if (quote) {
|
|
4472
|
-
throw new Error("Redis \u6307\u4EE4\u542B\u6709\u672A\u9589\u5408\u7684\u5F15\u865F");
|
|
4473
|
-
}
|
|
4474
|
-
if (inToken)
|
|
4475
|
-
push();
|
|
4476
|
-
return tokens;
|
|
4477
|
-
}
|
|
4478
|
-
async function scanAllKeys(client, pattern, count, maxKeys, accept = () => true) {
|
|
4479
|
-
const seen = new Set;
|
|
4480
|
-
const kept = new Set;
|
|
4481
|
-
let cursor = "0";
|
|
4482
|
-
do {
|
|
4483
|
-
const reply = await client.send("SCAN", [
|
|
4484
|
-
cursor,
|
|
4485
|
-
"MATCH",
|
|
4486
|
-
pattern,
|
|
4487
|
-
"COUNT",
|
|
4488
|
-
String(count)
|
|
4489
|
-
]);
|
|
4490
|
-
const [next, batch] = reply;
|
|
4491
|
-
for (const k of batch) {
|
|
4492
|
-
if (seen.has(k))
|
|
4493
|
-
continue;
|
|
4494
|
-
seen.add(k);
|
|
4495
|
-
if (accept(k))
|
|
4496
|
-
kept.add(k);
|
|
4497
|
-
}
|
|
4498
|
-
cursor = next;
|
|
4499
|
-
if (seen.size >= maxKeys)
|
|
4500
|
-
break;
|
|
4501
|
-
} while (cursor !== "0");
|
|
4502
|
-
return { keys: Array.from(kept), scanned: seen.size };
|
|
4503
|
-
}
|
|
4504
|
-
async function readSizeInfo(client, key, type) {
|
|
4505
|
-
switch (type) {
|
|
4506
|
-
case "string": {
|
|
4507
|
-
const len = await client.send("STRLEN", [key]);
|
|
4508
|
-
return { size: len };
|
|
4509
|
-
}
|
|
4510
|
-
case "hash": {
|
|
4511
|
-
const len = await client.send("HLEN", [key]);
|
|
4512
|
-
const fields = await client.send("HKEYS", [key]);
|
|
4513
|
-
return { size: len, sample: fields.slice(0, 5).join(", ") };
|
|
4514
|
-
}
|
|
4515
|
-
case "list": {
|
|
4516
|
-
const len = await client.send("LLEN", [key]);
|
|
4517
|
-
return { size: len };
|
|
4518
|
-
}
|
|
4519
|
-
case "set": {
|
|
4520
|
-
const len = await client.send("SCARD", [key]);
|
|
4521
|
-
return { size: len };
|
|
4522
|
-
}
|
|
4523
|
-
case "zset": {
|
|
4524
|
-
const len = await client.send("ZCARD", [key]);
|
|
4525
|
-
return { size: len };
|
|
4526
|
-
}
|
|
4527
|
-
case "stream": {
|
|
4528
|
-
const len = await client.send("XLEN", [key]);
|
|
4529
|
-
return { size: len };
|
|
4530
|
-
}
|
|
4531
|
-
default:
|
|
4532
|
-
return { size: 0 };
|
|
4533
|
-
}
|
|
4534
|
-
}
|
|
4535
|
-
function wrapReply(command, reply) {
|
|
4536
|
-
const upper = command.toUpperCase();
|
|
4537
|
-
if (reply === null || reply === undefined)
|
|
4538
|
-
return [];
|
|
4539
|
-
if (typeof reply === "string" || typeof reply === "number" || typeof reply === "boolean" || Buffer.isBuffer(reply)) {
|
|
4540
|
-
return [{ value: reply.toString() }];
|
|
4541
|
-
}
|
|
4542
|
-
if (Array.isArray(reply)) {
|
|
4543
|
-
if (upper === "HGETALL") {
|
|
4544
|
-
const obj = {};
|
|
4545
|
-
for (let i = 0;i < reply.length; i += 2) {
|
|
4546
|
-
obj[String(reply[i])] = reply[i + 1];
|
|
4547
|
-
}
|
|
4548
|
-
return [obj];
|
|
4549
|
-
}
|
|
4550
|
-
return reply.map((value, idx) => ({ index: idx, value: String(value) }));
|
|
4551
|
-
}
|
|
4552
|
-
if (typeof reply === "object") {
|
|
4553
|
-
return [reply];
|
|
4554
|
-
}
|
|
4555
|
-
return [{ value: String(reply) }];
|
|
4556
|
-
}
|
|
4557
|
-
|
|
4558
|
-
// src/adapters/elasticsearch-adapter.ts
|
|
4559
|
-
class ElasticsearchAdapter {
|
|
4560
|
-
options;
|
|
4561
|
-
baseUrl = "";
|
|
4562
|
-
currentNodeIndex = 0;
|
|
4563
|
-
constructor(options) {
|
|
4564
|
-
this.options = options;
|
|
4565
|
-
this.baseUrl = this.resolveBaseUrl();
|
|
4566
|
-
}
|
|
4567
|
-
async connect() {
|
|
4568
|
-
try {
|
|
4569
|
-
await this.request("GET", "/");
|
|
4570
|
-
} catch (error) {
|
|
4571
|
-
if (error instanceof ConnectionError)
|
|
4572
|
-
throw error;
|
|
4573
|
-
throw new ConnectionError("UNKNOWN", `Could not connect to Elasticsearch: ${error.message}`, ["Check if Elasticsearch is running", "Verify host and port", "Check network connectivity"]);
|
|
4574
|
-
}
|
|
4575
|
-
}
|
|
4576
|
-
async disconnect() {}
|
|
4577
|
-
async execute(query, params, options) {
|
|
4578
|
-
const indexName = params?.[0];
|
|
4579
|
-
if (!indexName) {
|
|
4580
|
-
throw new Error("Index name is required as the first parameter to execute()");
|
|
4581
|
-
}
|
|
4582
|
-
const isDsl = query.trim().startsWith("{");
|
|
4583
|
-
const limit = options?.limit ?? 100;
|
|
4584
|
-
let path = `/${encodeEsIndexExpression(indexName)}/_search`;
|
|
4585
|
-
let body = undefined;
|
|
4586
|
-
if (isDsl) {
|
|
4587
|
-
body = JSON.parse(query);
|
|
4588
|
-
assertNoElasticsearchScript(body);
|
|
4589
|
-
if (body.size === undefined) {
|
|
4590
|
-
body.size = limit;
|
|
4591
|
-
}
|
|
4592
|
-
} else {
|
|
4593
|
-
path += `?q=${encodeURIComponent(query)}&size=${limit}`;
|
|
4594
|
-
}
|
|
4595
|
-
const response = await this.request(body ? "POST" : "GET", path, body);
|
|
4596
|
-
const hits = response.hits?.hits || [];
|
|
4597
|
-
const rows = hits.map((hit) => {
|
|
4598
|
-
const row = { _id: hit._id };
|
|
4599
|
-
this.flattenSource("", hit._source || {}, row);
|
|
4600
|
-
return row;
|
|
4601
|
-
});
|
|
4602
|
-
return {
|
|
4603
|
-
rows,
|
|
4604
|
-
affectedRows: rows.length,
|
|
4605
|
-
rowCount: rows.length,
|
|
4606
|
-
columnNames: rows[0] ? Object.keys(rows[0]) : []
|
|
4607
|
-
};
|
|
4608
|
-
}
|
|
4609
|
-
flattenSource(prefix, source, target) {
|
|
4610
|
-
for (const [key, value] of Object.entries(source)) {
|
|
4611
|
-
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
4612
|
-
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
4613
|
-
this.flattenSource(fullKey, value, target);
|
|
4614
|
-
} else {
|
|
4615
|
-
target[fullKey] = value;
|
|
4616
|
-
}
|
|
4617
|
-
}
|
|
4618
|
-
}
|
|
4619
|
-
async listCollections(options) {
|
|
4620
|
-
const rows = await this.request("GET", "/_cat/indices?format=json&h=index,docs.count&expand_wildcards=all");
|
|
4621
|
-
return (Array.isArray(rows) ? rows : []).filter((row) => typeof row.index === "string" && row.index.length > 0).filter((row) => options?.includeSystem || !row.index.startsWith(".")).map((row) => {
|
|
4622
|
-
const parsed = Number(row["docs.count"]);
|
|
4623
|
-
return { name: row.index, documentCount: Number.isFinite(parsed) ? parsed : 0 };
|
|
4624
|
-
});
|
|
4625
|
-
}
|
|
4626
|
-
async listTables(options) {
|
|
4627
|
-
const collections = await this.listCollections(options);
|
|
4628
|
-
return collections.map((c) => ({
|
|
4629
|
-
name: c.name,
|
|
4630
|
-
columns: [],
|
|
4631
|
-
estimatedRowCount: c.documentCount,
|
|
4632
|
-
tableType: "table"
|
|
4633
|
-
}));
|
|
4634
|
-
}
|
|
4635
|
-
async getTableSchema(tableName, _options) {
|
|
4636
|
-
const mapping = await this.request("GET", `/${encodeEsIndexExpression(tableName)}/_mapping`);
|
|
4637
|
-
const indexMapping = mapping[tableName] || Object.values(mapping)[0];
|
|
4638
|
-
if (!indexMapping) {
|
|
4639
|
-
throw new Error(`Index not found: ${tableName}`);
|
|
4640
|
-
}
|
|
4641
|
-
const columns = [];
|
|
4642
|
-
this.flattenProperties("", indexMapping.mappings?.properties || {}, columns);
|
|
4643
|
-
return {
|
|
4644
|
-
name: tableName,
|
|
4645
|
-
columns,
|
|
4646
|
-
tableType: "table"
|
|
4647
|
-
};
|
|
4648
|
-
}
|
|
4649
|
-
flattenProperties(prefix, properties, columns) {
|
|
4650
|
-
for (const [name, config] of Object.entries(properties)) {
|
|
4651
|
-
const fullName = prefix ? `${prefix}.${name}` : name;
|
|
4652
|
-
if (config.properties) {
|
|
4653
|
-
this.flattenProperties(fullName, config.properties, columns);
|
|
4654
|
-
} else {
|
|
4655
|
-
columns.push({
|
|
4656
|
-
name: fullName,
|
|
4657
|
-
type: config.type || "object",
|
|
4658
|
-
nullable: true
|
|
4659
|
-
});
|
|
4660
|
-
if (config.fields) {
|
|
4661
|
-
for (const [fieldName, fieldConfig] of Object.entries(config.fields)) {
|
|
4662
|
-
columns.push({
|
|
4663
|
-
name: `${fullName}.${fieldName}`,
|
|
4664
|
-
type: fieldConfig.type,
|
|
4665
|
-
nullable: true
|
|
4666
|
-
});
|
|
4667
|
-
}
|
|
4668
|
-
}
|
|
4669
|
-
}
|
|
4670
|
-
}
|
|
4671
|
-
}
|
|
4672
|
-
async testConnection() {
|
|
4673
|
-
try {
|
|
4674
|
-
await this.connect();
|
|
4675
|
-
return true;
|
|
4676
|
-
} catch {
|
|
4677
|
-
return false;
|
|
4678
|
-
}
|
|
4679
|
-
}
|
|
4680
|
-
async getServerVersion() {
|
|
4681
|
-
const info = await this.request("GET", "/");
|
|
4682
|
-
return info.version?.number ?? "unknown";
|
|
4683
|
-
}
|
|
4684
|
-
async insert(collection, data) {
|
|
4685
|
-
const id = data._id || data.id;
|
|
4686
|
-
const body = { ...data };
|
|
4687
|
-
delete body._id;
|
|
4688
|
-
delete body.id;
|
|
4689
|
-
const encodedId = id ? encodeEsPathSegment(id) : "";
|
|
4690
|
-
const response = await this.request("PUT", `/${encodeEsIndexExpression(collection)}/_doc/${encodedId}`, body);
|
|
4691
|
-
return {
|
|
4692
|
-
rows: [],
|
|
4693
|
-
affectedRows: response.result === "created" || response.result === "updated" ? 1 : 0,
|
|
4694
|
-
lastInsertId: response._id
|
|
4695
|
-
};
|
|
4696
|
-
}
|
|
4697
|
-
async update(collection, filter, update) {
|
|
4698
|
-
const id = filter._id || filter.id;
|
|
4699
|
-
if (!id) {
|
|
4700
|
-
throw new Error("Elasticsearch update requires _id or id in filter");
|
|
4701
|
-
}
|
|
4702
|
-
const response = await this.request("POST", `/${encodeEsIndexExpression(collection)}/_update/${encodeEsPathSegment(id)}`, {
|
|
4703
|
-
doc: update
|
|
4704
|
-
});
|
|
4705
|
-
return {
|
|
4706
|
-
rows: [],
|
|
4707
|
-
affectedRows: response.result === "updated" || response.result === "noop" ? 1 : 0
|
|
4708
|
-
};
|
|
4709
|
-
}
|
|
4710
|
-
async delete(collection, filter) {
|
|
4711
|
-
const id = filter._id || filter.id;
|
|
4712
|
-
if (!id) {
|
|
4713
|
-
throw new Error("Elasticsearch delete requires _id or id in filter");
|
|
4714
|
-
}
|
|
4715
|
-
const response = await this.request("DELETE", `/${encodeEsIndexExpression(collection)}/_doc/${encodeEsPathSegment(id)}`);
|
|
4716
|
-
return {
|
|
4717
|
-
rows: [],
|
|
4718
|
-
affectedRows: response.result === "deleted" ? 1 : 0
|
|
4719
|
-
};
|
|
4720
|
-
}
|
|
4721
|
-
resolveBaseUrl() {
|
|
4722
|
-
if (this.options.cloudId) {
|
|
4723
|
-
return this.decodeCloudId(this.options.cloudId);
|
|
4724
|
-
}
|
|
4725
|
-
const nodes = this.options.nodes;
|
|
4726
|
-
if (nodes && nodes.length > 0) {
|
|
4727
|
-
return (nodes[0] ?? "").replace(/\/$/, "");
|
|
4728
|
-
}
|
|
4729
|
-
const protocol = this.options.protocol ?? "https";
|
|
4730
|
-
const host = this.options.host || "localhost";
|
|
4731
|
-
const port = this.options.port || 9200;
|
|
4732
|
-
return `${protocol}://${host}:${port}`;
|
|
4733
|
-
}
|
|
4734
|
-
decodeCloudId(cloudId) {
|
|
4735
|
-
try {
|
|
4736
|
-
const parts = cloudId.split(":");
|
|
4737
|
-
if (parts.length !== 2)
|
|
4738
|
-
throw new Error("Invalid format");
|
|
4739
|
-
const base64 = parts[1] ?? "";
|
|
4740
|
-
const decoded = atob(base64);
|
|
4741
|
-
const [hostAndPort = "", ...rest] = decoded.split("$");
|
|
4742
|
-
if (hostAndPort.includes(".")) {
|
|
4743
|
-
const [host = "", port] = hostAndPort.split(":");
|
|
4744
|
-
return `https://${rest[0]}.${host}:${port || 443}`;
|
|
4745
|
-
}
|
|
4746
|
-
return `https://${rest[0]}.${hostAndPort}:443`;
|
|
4747
|
-
} catch (error) {
|
|
4748
|
-
throw new Error(`Invalid Cloud ID: ${error.message}`);
|
|
4749
|
-
}
|
|
4750
|
-
}
|
|
4751
|
-
async getHeaders() {
|
|
4752
|
-
const headers = {
|
|
4753
|
-
"Content-Type": "application/json"
|
|
4754
|
-
};
|
|
4755
|
-
if (this.options.apiKey) {
|
|
4756
|
-
headers["Authorization"] = `ApiKey ${this.options.apiKey}`;
|
|
4757
|
-
} else if (this.options.user && this.options.password) {
|
|
4758
|
-
const auth = btoa(`${this.options.user}:${this.options.password}`);
|
|
4759
|
-
headers["Authorization"] = `Basic ${auth}`;
|
|
4760
|
-
}
|
|
4761
|
-
return headers;
|
|
4762
|
-
}
|
|
4763
|
-
getBaseUrl() {
|
|
4764
|
-
return this.baseUrl;
|
|
4765
|
-
}
|
|
4766
|
-
async request(method, path, body, options) {
|
|
4767
|
-
const url = `${this.baseUrl}${path.startsWith("/") ? "" : "/"}${path}`;
|
|
4768
|
-
const headers = await this.getHeaders();
|
|
4769
|
-
const controller = new AbortController;
|
|
4770
|
-
const timeout = options?.timeout ?? this.options.timeout ?? 5000;
|
|
4771
|
-
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
4772
|
-
try {
|
|
4773
|
-
const response = await fetch(url, {
|
|
4774
|
-
method,
|
|
4775
|
-
headers,
|
|
4776
|
-
body: body ? typeof body === "string" ? body : JSON.stringify(body) : undefined,
|
|
4777
|
-
signal: controller.signal,
|
|
4778
|
-
tls: {
|
|
4779
|
-
rejectUnauthorized: this.options.rejectUnauthorized ?? true,
|
|
4780
|
-
ca: this.options.caPath ? await Bun.file(this.options.caPath).text() : undefined
|
|
4781
|
-
}
|
|
4782
|
-
});
|
|
4783
|
-
if (!response.ok) {
|
|
4784
|
-
await this.handleErrorResponse(response);
|
|
4785
|
-
}
|
|
4786
|
-
return await response.json();
|
|
4787
|
-
} catch (error) {
|
|
4788
|
-
if (error.name === "AbortError") {
|
|
4789
|
-
throw new ConnectionError("ETIMEDOUT", `Request timed out after ${timeout}ms`, [
|
|
4790
|
-
"Increase timeout in config if the cluster is slow",
|
|
4791
|
-
"Check if the network is congested"
|
|
4792
|
-
]);
|
|
4793
|
-
}
|
|
4794
|
-
if (error instanceof ConnectionError)
|
|
4795
|
-
throw error;
|
|
4796
|
-
const msg = error.message.toLowerCase();
|
|
4797
|
-
if (msg.includes("connection refused") || msg.includes("econnrefused")) {
|
|
4798
|
-
throw new ConnectionError("ECONNREFUSED", `Connection refused at ${this.baseUrl}`, [
|
|
4799
|
-
"Check if Elasticsearch is running",
|
|
4800
|
-
"Verify host and port"
|
|
4801
|
-
]);
|
|
4802
|
-
}
|
|
4803
|
-
if (msg.includes("not found") || msg.includes("enotfound")) {
|
|
4804
|
-
throw new ConnectionError("ENOTFOUND", `Host not found: ${this.baseUrl}`, [
|
|
4805
|
-
"Verify the hostname is correct",
|
|
4806
|
-
"Check DNS settings"
|
|
4807
|
-
]);
|
|
4808
|
-
}
|
|
4809
|
-
throw error;
|
|
4810
|
-
} finally {
|
|
4811
|
-
clearTimeout(timeoutId);
|
|
4812
|
-
}
|
|
4813
|
-
}
|
|
4814
|
-
async handleErrorResponse(response) {
|
|
4815
|
-
let message = "";
|
|
4816
|
-
try {
|
|
4817
|
-
const errorBody = await response.json();
|
|
4818
|
-
message = errorBody?.error?.reason || errorBody?.message || response.statusText;
|
|
4819
|
-
} catch {
|
|
4820
|
-
try {
|
|
4821
|
-
message = await response.text();
|
|
4822
|
-
} catch {
|
|
4823
|
-
message = response.statusText;
|
|
4824
|
-
}
|
|
4825
|
-
}
|
|
4826
|
-
const status = response.status;
|
|
4827
|
-
if (status === 401) {
|
|
4828
|
-
throw new ConnectionError("AUTH_FAILED", `Authentication failed: ${message}`, [
|
|
4829
|
-
"Check your API key or username/password",
|
|
4830
|
-
"Verify if the user has required roles"
|
|
4831
|
-
]);
|
|
4832
|
-
}
|
|
4833
|
-
if (status === 403) {
|
|
4834
|
-
throw new ConnectionError("AUTH_FAILED", `Permission denied: ${message}`, [
|
|
4835
|
-
"User does not have permission to perform this action"
|
|
4836
|
-
]);
|
|
4837
|
-
}
|
|
4838
|
-
throw new ConnectionError("UNKNOWN", `Elasticsearch error (${status}): ${message}`, [
|
|
4839
|
-
"Check Elasticsearch logs for more details",
|
|
4840
|
-
"Verify the request body is valid ES DSL"
|
|
4841
|
-
]);
|
|
4842
|
-
}
|
|
4843
|
-
}
|
|
4844
|
-
|
|
4845
|
-
// src/utils/connection-timeout.ts
|
|
4846
|
-
var _globalConnectionTimeout;
|
|
4847
|
-
var _globalStatementTimeout;
|
|
4848
|
-
function resolveConnectionTimeout(configured) {
|
|
4849
|
-
return _globalConnectionTimeout ?? configured;
|
|
4850
|
-
}
|
|
4851
|
-
function resolveStatementTimeout(configured) {
|
|
4852
|
-
return _globalStatementTimeout ?? configured;
|
|
4853
|
-
}
|
|
4854
|
-
function withResolvedTimeout(options) {
|
|
4855
|
-
const timeout = resolveConnectionTimeout(options.timeout);
|
|
4856
|
-
const statementTimeout = resolveStatementTimeout(options.statementTimeout);
|
|
4857
|
-
if (timeout === options.timeout && statementTimeout === options.statementTimeout)
|
|
4858
|
-
return options;
|
|
4859
|
-
return { ...options, timeout, statementTimeout };
|
|
4860
|
-
}
|
|
4861
|
-
|
|
4862
|
-
// src/adapters/factory.ts
|
|
4863
|
-
class AdapterFactory {
|
|
4864
|
-
static createSqlAdapter(rawOptions) {
|
|
4865
|
-
const options = withResolvedTimeout(rawOptions);
|
|
4866
|
-
switch (options.system) {
|
|
4867
|
-
case "postgresql":
|
|
4868
|
-
return new PostgreSQLAdapter(options);
|
|
4869
|
-
case "mysql":
|
|
4870
|
-
case "mariadb":
|
|
4871
|
-
return new MySQLAdapter(options);
|
|
4872
|
-
default:
|
|
4873
|
-
throw new Error(`createSqlAdapter requires a SQL system, got: ${options.system}`);
|
|
4874
|
-
}
|
|
4875
|
-
}
|
|
4876
|
-
static createQueryableAdapter(rawOptions) {
|
|
4877
|
-
const options = withResolvedTimeout(rawOptions);
|
|
4878
|
-
switch (options.system) {
|
|
4879
|
-
case "mongodb":
|
|
4880
|
-
return new MongoDBAdapter(options);
|
|
4881
|
-
case "redis":
|
|
4882
|
-
return new RedisAdapter(options);
|
|
4883
|
-
case "elasticsearch":
|
|
4884
|
-
return new ElasticsearchAdapter(options);
|
|
4885
|
-
default:
|
|
4886
|
-
throw new Error(`createQueryableAdapter requires a non-SQL queryable system, got: ${options.system}`);
|
|
4887
|
-
}
|
|
4888
|
-
}
|
|
4889
|
-
static createAdapter(options) {
|
|
4890
|
-
switch (options.system) {
|
|
4891
|
-
case "postgresql":
|
|
4892
|
-
case "mysql":
|
|
4893
|
-
case "mariadb":
|
|
4894
|
-
return AdapterFactory.createSqlAdapter(options);
|
|
4895
|
-
case "mongodb":
|
|
4896
|
-
case "redis":
|
|
4897
|
-
case "elasticsearch":
|
|
4898
|
-
return AdapterFactory.createQueryableAdapter(options);
|
|
4899
|
-
default:
|
|
4900
|
-
throw new Error(`Unsupported database system: ${options.system}`);
|
|
4901
|
-
}
|
|
4902
|
-
}
|
|
4903
|
-
static createMongoDBAdapter(options) {
|
|
4904
|
-
if (options.system !== "mongodb") {
|
|
4905
|
-
throw new Error("createMongoDBAdapter requires system: mongodb");
|
|
4906
|
-
}
|
|
4907
|
-
return AdapterFactory.createQueryableAdapter(options);
|
|
4908
|
-
}
|
|
4909
|
-
static createRedisAdapter(rawOptions, blacklistRules = [], maskRules = []) {
|
|
4910
|
-
if (rawOptions.system !== "redis") {
|
|
4911
|
-
throw new Error("createRedisAdapter requires system: redis");
|
|
4912
|
-
}
|
|
4913
|
-
const options = withResolvedTimeout(rawOptions);
|
|
4914
|
-
const adapter = new RedisAdapter(options);
|
|
4915
|
-
adapter.setBlacklistRules(blacklistRules);
|
|
4916
|
-
adapter.setMaskRules(maskRules);
|
|
4917
|
-
return adapter;
|
|
4918
|
-
}
|
|
4919
|
-
static createElasticsearchAdapter(options) {
|
|
4920
|
-
if (options.system !== "elasticsearch") {
|
|
4921
|
-
throw new Error("createElasticsearchAdapter requires system: elasticsearch");
|
|
4922
|
-
}
|
|
4923
|
-
return AdapterFactory.createQueryableAdapter(options);
|
|
4924
|
-
}
|
|
4925
|
-
}
|
|
4926
2527
|
// src/utils/sql-lexical.ts
|
|
4927
2528
|
var IDENTIFIER_CONTINUATION = /[A-Za-z0-9_$]|[\u0080-\uFFFF]/;
|
|
4928
2529
|
var IDENTIFIER_START = /[A-Za-z_]|[\u0080-\uFFFF]/;
|
|
@@ -10349,10 +7950,26 @@ var MetadataSchema = exports_external.object({
|
|
|
10349
7950
|
schemaLastUpdated: exports_external.string().datetime().optional(),
|
|
10350
7951
|
schemaTableCount: exports_external.number().int().nonnegative().optional()
|
|
10351
7952
|
}).optional().default({});
|
|
7953
|
+
var BLACKLIST_KEY_ALIASES = {
|
|
7954
|
+
indices: "tables",
|
|
7955
|
+
index: "tables",
|
|
7956
|
+
fields: "columns",
|
|
7957
|
+
keys: "tables"
|
|
7958
|
+
};
|
|
10352
7959
|
var BlacklistConfigSchema = exports_external.object({
|
|
10353
7960
|
tables: exports_external.array(exports_external.string()).default([]),
|
|
10354
7961
|
columns: exports_external.record(exports_external.array(exports_external.string())).default({})
|
|
10355
|
-
}).
|
|
7962
|
+
}).passthrough().superRefine((value, ctx) => {
|
|
7963
|
+
for (const [wrong, right] of Object.entries(BLACKLIST_KEY_ALIASES)) {
|
|
7964
|
+
if (wrong in value) {
|
|
7965
|
+
ctx.addIssue({
|
|
7966
|
+
code: exports_external.ZodIssueCode.custom,
|
|
7967
|
+
path: [wrong],
|
|
7968
|
+
message: `blacklist.${wrong} is not a setting and would be silently ignored \u2014 did you mean blacklist.${right}?`
|
|
7969
|
+
});
|
|
7970
|
+
}
|
|
7971
|
+
}
|
|
7972
|
+
}).transform((value) => ({ tables: value.tables, columns: value.columns })).optional().default({ tables: [], columns: {} });
|
|
10356
7973
|
var RedisMaskRuleSchema = exports_external.object({
|
|
10357
7974
|
keyPattern: exports_external.string().min(1),
|
|
10358
7975
|
fields: exports_external.array(exports_external.string()).optional()
|
|
@@ -10366,9 +7983,14 @@ var AuditRotationConfigSchema = exports_external.object({
|
|
|
10366
7983
|
}).optional().default({ max_bytes: 10485760, max_entries: 1000 });
|
|
10367
7984
|
var AuditConfigSchema = exports_external.object({
|
|
10368
7985
|
enabled: exports_external.boolean().default(true),
|
|
7986
|
+
strict: exports_external.boolean().default(false),
|
|
10369
7987
|
rotation: AuditRotationConfigSchema
|
|
7988
|
+
}).refine((audit) => !(audit.strict && !audit.enabled), {
|
|
7989
|
+
message: "audit.strict requires audit.enabled: with audit off there is nothing to write, so " + "strict would refuse nothing. Enable audit, or turn strict off.",
|
|
7990
|
+
path: ["strict"]
|
|
10370
7991
|
}).optional().default({
|
|
10371
7992
|
enabled: true,
|
|
7993
|
+
strict: false,
|
|
10372
7994
|
rotation: { max_bytes: 10485760, max_entries: 1000 }
|
|
10373
7995
|
});
|
|
10374
7996
|
var DbcliConfigSchema = exports_external.object({
|
|
@@ -10400,12 +8022,24 @@ var ElasticsearchNamedConnectionSchema = ElasticsearchConnectionConfigSchema.ext
|
|
|
10400
8022
|
envFile: exports_external.string().optional(),
|
|
10401
8023
|
environment: EnvironmentLabelSchema
|
|
10402
8024
|
});
|
|
10403
|
-
var
|
|
8025
|
+
var NamedConnectionUnion = exports_external.union([
|
|
10404
8026
|
SqlNamedConnectionSchema,
|
|
10405
8027
|
MongoDBNamedConnectionSchema,
|
|
10406
8028
|
RedisNamedConnectionSchema,
|
|
10407
8029
|
ElasticsearchNamedConnectionSchema
|
|
10408
8030
|
]);
|
|
8031
|
+
var NamedConnectionSchema = exports_external.preprocess((raw, ctx) => {
|
|
8032
|
+
if (raw !== null && typeof raw === "object" && !Array.isArray(raw)) {
|
|
8033
|
+
if ("blacklist" in raw) {
|
|
8034
|
+
ctx.addIssue({
|
|
8035
|
+
code: exports_external.ZodIssueCode.custom,
|
|
8036
|
+
path: ["blacklist"],
|
|
8037
|
+
message: "blacklist is configured once at the top level, not per connection \u2014 a blacklist here " + "would be ignored. Move it to the top-level blacklist."
|
|
8038
|
+
});
|
|
8039
|
+
}
|
|
8040
|
+
}
|
|
8041
|
+
return raw;
|
|
8042
|
+
}, NamedConnectionUnion);
|
|
10409
8043
|
var DbcliConfigV2Schema = exports_external.object({
|
|
10410
8044
|
version: exports_external.literal(2),
|
|
10411
8045
|
default: exports_external.string().min(1),
|
|
@@ -11146,6 +8780,7 @@ var DEFAULT_CONFIG = {
|
|
|
11146
8780
|
blacklist: { tables: [], columns: {} },
|
|
11147
8781
|
audit: {
|
|
11148
8782
|
enabled: true,
|
|
8783
|
+
strict: false,
|
|
11149
8784
|
rotation: { max_bytes: 10485760, max_entries: 1000 }
|
|
11150
8785
|
}
|
|
11151
8786
|
};
|
|
@@ -11535,6 +9170,32 @@ function matchesIndexGlob(pattern, name) {
|
|
|
11535
9170
|
return true;
|
|
11536
9171
|
}
|
|
11537
9172
|
}
|
|
9173
|
+
function reachesByConvention(name, entry) {
|
|
9174
|
+
const lower = name.toLowerCase();
|
|
9175
|
+
const target = entry.toLowerCase();
|
|
9176
|
+
return new RegExp(`^\\.ds-${escapeRegExp(target)}-`).test(lower) || new RegExp(`^${escapeRegExp(target)}-\\d+$`).test(lower);
|
|
9177
|
+
}
|
|
9178
|
+
function escapeRegExp(text) {
|
|
9179
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
9180
|
+
}
|
|
9181
|
+
function expandBlacklistEntries(blacklisted) {
|
|
9182
|
+
const names = [];
|
|
9183
|
+
const patterns = [];
|
|
9184
|
+
for (const entry of blacklisted) {
|
|
9185
|
+
const { concrete, wildcards } = expandIndexTargets(entry);
|
|
9186
|
+
names.push(...concrete);
|
|
9187
|
+
patterns.push(...wildcards);
|
|
9188
|
+
}
|
|
9189
|
+
return { names, patterns };
|
|
9190
|
+
}
|
|
9191
|
+
function indexExpressionReaches(expression, blacklisted) {
|
|
9192
|
+
if (blacklisted.length === 0)
|
|
9193
|
+
return false;
|
|
9194
|
+
const { concrete, wildcards } = expandIndexTargets(expression);
|
|
9195
|
+
const entries = expandBlacklistEntries(blacklisted);
|
|
9196
|
+
const reachesName = (name) => entries.names.some((entry) => entry.toLowerCase() === name.toLowerCase() || reachesByConvention(name, entry)) || entries.patterns.some((pattern) => matchesIndexGlob(pattern, name));
|
|
9197
|
+
return concrete.some(reachesName) || wildcards.some((pattern) => entries.names.some((entry) => matchesIndexGlob(pattern, entry)) || entries.patterns.length > 0);
|
|
9198
|
+
}
|
|
11538
9199
|
|
|
11539
9200
|
// src/core/blacklist-validator.ts
|
|
11540
9201
|
function flattenArrayRow(row) {
|
|
@@ -11592,26 +9253,23 @@ class BlacklistValidator {
|
|
|
11592
9253
|
throw new BlacklistError(message, blocked[0], operation);
|
|
11593
9254
|
}
|
|
11594
9255
|
checkIndexBlacklist(operation, target) {
|
|
11595
|
-
|
|
11596
|
-
this.checkTablesBlacklist(operation, concrete);
|
|
11597
|
-
if (wildcards.length === 0 || this.manager.canOverrideBlacklist())
|
|
9256
|
+
if (this.manager.canOverrideBlacklist())
|
|
11598
9257
|
return;
|
|
11599
9258
|
const blacklisted = Array.from(this.manager.getState().tables);
|
|
11600
9259
|
if (blacklisted.length === 0)
|
|
11601
9260
|
return;
|
|
11602
|
-
|
|
11603
|
-
if (reachable.length === 0)
|
|
9261
|
+
if (!indexExpressionReaches(target, blacklisted))
|
|
11604
9262
|
return;
|
|
11605
9263
|
const message = t_vars("errors.table_blacklisted", {
|
|
11606
|
-
table:
|
|
9264
|
+
table: target,
|
|
11607
9265
|
operation
|
|
11608
9266
|
});
|
|
11609
|
-
throw new BlacklistError(message,
|
|
9267
|
+
throw new BlacklistError(message, target, operation);
|
|
11610
9268
|
}
|
|
11611
9269
|
filterColumnsForIndexExpression(target, rows, columnList) {
|
|
11612
9270
|
const { concrete, wildcards } = expandIndexTargets(target);
|
|
11613
9271
|
const ruleKeys = Array.from(this.manager.getState().columns.keys());
|
|
11614
|
-
const reachable = ruleKeys.filter((key) => wildcards.some((pattern) => matchesIndexGlob(pattern, key)));
|
|
9272
|
+
const reachable = ruleKeys.filter((key) => wildcards.some((pattern) => matchesIndexGlob(pattern, key)) || concrete.some((name) => indexExpressionReaches(name, [key])));
|
|
11615
9273
|
return this.filterColumnsForTables([...concrete, ...reachable], rows, columnList);
|
|
11616
9274
|
}
|
|
11617
9275
|
checkColumnBlacklistOnWrite(tableName, fields, operation = "WRITE") {
|
|
@@ -11742,6 +9400,5 @@ export {
|
|
|
11742
9400
|
ConnectionError,
|
|
11743
9401
|
BlacklistValidator,
|
|
11744
9402
|
BlacklistManager,
|
|
11745
|
-
BlacklistError
|
|
11746
|
-
AdapterFactory
|
|
9403
|
+
BlacklistError
|
|
11747
9404
|
};
|