@carllee1983/dbcli 1.42.0 → 1.43.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/rules/dbcli.mdc +29 -5
- package/.cursor/skills/dbcli/reference.md +79 -4
- package/.cursor-plugin/plugin.json +1 -1
- package/.github/skills/dbcli/SKILL.md +29 -5
- package/.github/skills/dbcli/reference.md +79 -4
- package/CHANGELOG.md +32 -0
- package/README.dev.md +1 -1
- package/README.md +72 -10
- package/README.zh-TW.md +70 -10
- package/assets/SKILL.md +29 -5
- package/assets/SKILL.zh-TW.md +27 -4
- package/assets/reference.md +79 -4
- package/assets/ui-template.html +19 -19
- package/dist/agent-core.d.ts +32 -0
- package/dist/agent-core.mjs +92 -0
- package/dist/cli.mjs +1539 -686
- package/dist/core.d.ts +23 -0
- package/dist/core.mjs +323 -76
- package/dist/ui-style.css +1 -1
- package/gemini-extension.json +1 -1
- package/package.json +9 -3
- package/plugins/dbcli-agent/.codex-plugin/plugin.json +1 -1
- package/plugins/dbcli-agent/skills/dbcli/SKILL.md +29 -5
- package/plugins/dbcli-agent/skills/dbcli/reference.md +79 -4
- package/skills/dbcli/SKILL.md +29 -5
- package/skills/dbcli/reference.md +79 -4
package/dist/core.mjs
CHANGED
|
@@ -13574,6 +13574,8 @@ function getEngineCapability(system, key) {
|
|
|
13574
13574
|
}
|
|
13575
13575
|
// src/adapters/error-mapper.ts
|
|
13576
13576
|
function mapError(error, system, options) {
|
|
13577
|
+
if (error instanceof ConnectionError)
|
|
13578
|
+
return error;
|
|
13577
13579
|
const err = error;
|
|
13578
13580
|
const errMsg = String(err?.message || String(error));
|
|
13579
13581
|
const errCode = String(err?.code || "");
|
|
@@ -14920,7 +14922,7 @@ class MySQLAdapter {
|
|
|
14920
14922
|
JOIN information_schema.KEY_COLUMN_USAGE kcu
|
|
14921
14923
|
ON rc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME AND rc.TABLE_NAME = kcu.TABLE_NAME
|
|
14922
14924
|
WHERE kcu.TABLE_NAME = ? AND rc.CONSTRAINT_SCHEMA = DATABASE()
|
|
14923
|
-
GROUP BY rc.CONSTRAINT_NAME
|
|
14925
|
+
GROUP BY rc.CONSTRAINT_NAME, rc.REFERENCED_TABLE_NAME
|
|
14924
14926
|
`;
|
|
14925
14927
|
const fkResult = await this.execute(fkQuery, [tableName]);
|
|
14926
14928
|
const fkResults = fkResult.rows;
|
|
@@ -15152,13 +15154,14 @@ class MongoDBAdapter {
|
|
|
15152
15154
|
let docs;
|
|
15153
15155
|
if (Array.isArray(parsed)) {
|
|
15154
15156
|
const stages = parsed;
|
|
15155
|
-
const
|
|
15156
|
-
const
|
|
15157
|
-
const finalStages =
|
|
15157
|
+
const alreadyHasLimit = stages.some((stage) => stage !== null && typeof stage === "object" && ("$limit" in stage));
|
|
15158
|
+
const limitedStages = typeof limit === "number" && limit > 0 && !alreadyHasLimit ? [...stages, { $limit: limit }] : stages;
|
|
15159
|
+
const finalStages = options?.projection ? [...limitedStages, { $project: options.projection }] : limitedStages;
|
|
15158
15160
|
docs = await collection.aggregate(finalStages).toArray();
|
|
15159
15161
|
} else {
|
|
15160
15162
|
const cap2 = typeof limit === "number" && limit > 0 ? limit : 0;
|
|
15161
|
-
|
|
15163
|
+
const cursor = options?.projection ? collection.find(parsed, { projection: options.projection }) : collection.find(parsed);
|
|
15164
|
+
docs = await cursor.limit(cap2).toArray();
|
|
15162
15165
|
}
|
|
15163
15166
|
return { rows: docs, affectedRows: docs.length };
|
|
15164
15167
|
}
|
|
@@ -16743,24 +16746,48 @@ function normalizeSQL(sql) {
|
|
|
16743
16746
|
return sql.replace(/--[^\n]*\n/g, `
|
|
16744
16747
|
`).replace(/\/\*[\s\S]*?\*\//g, " ").trim().replace(/\s+/g, " ");
|
|
16745
16748
|
}
|
|
16746
|
-
function stripCommentsAndStrings(sql) {
|
|
16749
|
+
function stripCommentsAndStrings(sql, options = {}) {
|
|
16747
16750
|
let result = "";
|
|
16748
16751
|
let i = 0;
|
|
16749
16752
|
while (i < sql.length) {
|
|
16750
16753
|
const char = sql[i];
|
|
16751
|
-
|
|
16754
|
+
const mysqlDialect = options.dialect === "mysql" || options.dialect === "mariadb";
|
|
16755
|
+
const dashFollowerCode = sql.charCodeAt(i + 2);
|
|
16756
|
+
const dashStartsComment = char === "-" && sql[i + 1] === "-" && (!mysqlDialect || sql[i + 2] === undefined || dashFollowerCode <= 32 || dashFollowerCode === 127);
|
|
16757
|
+
if (dashStartsComment) {
|
|
16752
16758
|
while (i < sql.length && sql[i] !== `
|
|
16753
16759
|
`) {
|
|
16754
16760
|
i++;
|
|
16755
16761
|
}
|
|
16756
16762
|
if (i < sql.length) {
|
|
16757
16763
|
result += `
|
|
16764
|
+
`;
|
|
16765
|
+
i++;
|
|
16766
|
+
}
|
|
16767
|
+
continue;
|
|
16768
|
+
}
|
|
16769
|
+
if (mysqlDialect && char === "#") {
|
|
16770
|
+
while (i < sql.length && sql[i] !== `
|
|
16771
|
+
`)
|
|
16772
|
+
i++;
|
|
16773
|
+
if (i < sql.length) {
|
|
16774
|
+
result += `
|
|
16758
16775
|
`;
|
|
16759
16776
|
i++;
|
|
16760
16777
|
}
|
|
16761
16778
|
continue;
|
|
16762
16779
|
}
|
|
16763
16780
|
if (char === "/" && sql[i + 1] === "*") {
|
|
16781
|
+
const executableMysqlComment = mysqlDialect && (sql.startsWith("/*!", i) || sql.startsWith("/*M!", i));
|
|
16782
|
+
if (executableMysqlComment) {
|
|
16783
|
+
const prefixLength = sql.startsWith("/*M!", i) ? 4 : 3;
|
|
16784
|
+
const closingIndex = sql.indexOf("*/", i + prefixLength);
|
|
16785
|
+
const bodyEnd = closingIndex === -1 ? sql.length : closingIndex;
|
|
16786
|
+
const executableBody = sql.slice(i + prefixLength, bodyEnd).replace(/^\d+/, " ");
|
|
16787
|
+
result += " " + stripCommentsAndStrings(executableBody, options) + " ";
|
|
16788
|
+
i = closingIndex === -1 ? sql.length : closingIndex + 2;
|
|
16789
|
+
continue;
|
|
16790
|
+
}
|
|
16764
16791
|
i += 2;
|
|
16765
16792
|
while (i < sql.length) {
|
|
16766
16793
|
if (sql[i] === "*" && sql[i + 1] === "/") {
|
|
@@ -16772,16 +16799,51 @@ function stripCommentsAndStrings(sql) {
|
|
|
16772
16799
|
result += " ";
|
|
16773
16800
|
continue;
|
|
16774
16801
|
}
|
|
16775
|
-
if (
|
|
16776
|
-
const
|
|
16802
|
+
if (options.dialect === "postgresql" && char === "$") {
|
|
16803
|
+
const delimiter = sql.slice(i).match(/^\$(?:[A-Za-z_][A-Za-z0-9_]*)?\$/)?.[0];
|
|
16804
|
+
if (delimiter) {
|
|
16805
|
+
i += delimiter.length;
|
|
16806
|
+
const closingIndex = sql.indexOf(delimiter, i);
|
|
16807
|
+
i = closingIndex === -1 ? sql.length : closingIndex + delimiter.length;
|
|
16808
|
+
result += " ";
|
|
16809
|
+
continue;
|
|
16810
|
+
}
|
|
16811
|
+
}
|
|
16812
|
+
if (mysqlDialect && char === "`") {
|
|
16777
16813
|
i++;
|
|
16778
|
-
while (i < sql.length
|
|
16779
|
-
if (sql[i] === "
|
|
16814
|
+
while (i < sql.length) {
|
|
16815
|
+
if (sql[i] === "`") {
|
|
16816
|
+
if (sql[i + 1] === "`") {
|
|
16817
|
+
i += 2;
|
|
16818
|
+
continue;
|
|
16819
|
+
}
|
|
16780
16820
|
i++;
|
|
16821
|
+
break;
|
|
16781
16822
|
}
|
|
16782
16823
|
i++;
|
|
16783
16824
|
}
|
|
16784
|
-
|
|
16825
|
+
result += " ";
|
|
16826
|
+
continue;
|
|
16827
|
+
}
|
|
16828
|
+
if (char === "'" || char === '"') {
|
|
16829
|
+
const quote = char;
|
|
16830
|
+
const quoteIndex = i;
|
|
16831
|
+
const postgresEscapeString = options.dialect === "postgresql" && quote === "'" && /[eE]/.test(sql[quoteIndex - 1] ?? "") && !/[A-Za-z0-9_$]/.test(sql[quoteIndex - 2] ?? "");
|
|
16832
|
+
const backslashEscapes = options.dialect === undefined || postgresEscapeString;
|
|
16833
|
+
i++;
|
|
16834
|
+
while (i < sql.length) {
|
|
16835
|
+
if (sql[i] === quote) {
|
|
16836
|
+
if (sql[i + 1] === quote) {
|
|
16837
|
+
i += 2;
|
|
16838
|
+
continue;
|
|
16839
|
+
}
|
|
16840
|
+
i++;
|
|
16841
|
+
break;
|
|
16842
|
+
}
|
|
16843
|
+
if (backslashEscapes && sql[i] === "\\") {
|
|
16844
|
+
i += 2;
|
|
16845
|
+
continue;
|
|
16846
|
+
}
|
|
16785
16847
|
i++;
|
|
16786
16848
|
}
|
|
16787
16849
|
result += " ";
|
|
@@ -17144,6 +17206,14 @@ function getOperationTarget(system, command, options, sql) {
|
|
|
17144
17206
|
// src/core/limits.ts
|
|
17145
17207
|
var DEFAULT_QUERY_ONLY_LIMIT = 1000;
|
|
17146
17208
|
|
|
17209
|
+
// src/agent-core/applied-limit.ts
|
|
17210
|
+
function trimAppliedLimit(rows, limit) {
|
|
17211
|
+
const truncated = rows.length > limit;
|
|
17212
|
+
return {
|
|
17213
|
+
rows: truncated ? rows.slice(0, limit) : [...rows],
|
|
17214
|
+
metadata: { truncated, limitApplied: limit }
|
|
17215
|
+
};
|
|
17216
|
+
}
|
|
17147
17217
|
// src/core/audit/logger.ts
|
|
17148
17218
|
import { appendFile, mkdir as mkdir2, readFile, stat } from "fs/promises";
|
|
17149
17219
|
import { join } from "path";
|
|
@@ -21634,18 +21704,16 @@ var DbcliConfigV2Schema = exports_external.object({
|
|
|
21634
21704
|
path: ["default"]
|
|
21635
21705
|
});
|
|
21636
21706
|
|
|
21637
|
-
// src/
|
|
21707
|
+
// src/agent-core/errors.ts
|
|
21638
21708
|
class ConfigError extends Error {
|
|
21639
21709
|
constructor(message) {
|
|
21640
21710
|
super(message);
|
|
21641
21711
|
this.name = "ConfigError";
|
|
21642
|
-
if (Error.captureStackTrace)
|
|
21712
|
+
if (Error.captureStackTrace)
|
|
21643
21713
|
Error.captureStackTrace(this, ConfigError);
|
|
21644
|
-
}
|
|
21645
21714
|
}
|
|
21646
21715
|
}
|
|
21647
|
-
|
|
21648
|
-
// src/core/env-loader.ts
|
|
21716
|
+
// src/agent-core/env-loader.ts
|
|
21649
21717
|
function parseEnvContent(content) {
|
|
21650
21718
|
const entries = [];
|
|
21651
21719
|
for (const line of content.split(`
|
|
@@ -21667,19 +21735,13 @@ function parseEnvContent(content) {
|
|
|
21667
21735
|
}
|
|
21668
21736
|
async function loadEnvFile(filePath) {
|
|
21669
21737
|
const file = Bun.file(filePath);
|
|
21670
|
-
|
|
21671
|
-
if (!exists) {
|
|
21738
|
+
if (!await file.exists())
|
|
21672
21739
|
throw new ConfigError(`\u627E\u4E0D\u5230 env \u6A94\u6848\uFF1A${filePath}`);
|
|
21673
|
-
|
|
21674
|
-
|
|
21675
|
-
const entries = parseEnvContent(content);
|
|
21676
|
-
for (const [key, value] of entries) {
|
|
21677
|
-
if (process.env[key] === undefined) {
|
|
21740
|
+
for (const [key, value] of parseEnvContent(await file.text())) {
|
|
21741
|
+
if (process.env[key] === undefined)
|
|
21678
21742
|
process.env[key] = value;
|
|
21679
|
-
}
|
|
21680
21743
|
}
|
|
21681
21744
|
}
|
|
21682
|
-
|
|
21683
21745
|
// src/core/config-v2.ts
|
|
21684
21746
|
import { join as join4 } from "path";
|
|
21685
21747
|
import { mkdir as mkdir5, rename as rename3 } from "fs/promises";
|
|
@@ -21749,7 +21811,25 @@ function listConnections(config) {
|
|
|
21749
21811
|
// src/core/config.ts
|
|
21750
21812
|
import { join as join9 } from "path";
|
|
21751
21813
|
import { mkdir as mkdir6 } from "fs/promises";
|
|
21814
|
+
// src/agent-core/env-ref.ts
|
|
21815
|
+
function resolveEnvRef(value, fieldName, env = process.env) {
|
|
21816
|
+
if (typeof value === "string")
|
|
21817
|
+
return value;
|
|
21818
|
+
const envKey = value.$env;
|
|
21819
|
+
const resolved = env[envKey];
|
|
21820
|
+
if (resolved === undefined) {
|
|
21821
|
+
throw new ConfigError(`Environment variable not defined: ${envKey} (field: ${fieldName})
|
|
21822
|
+
` + `Please set ${envKey} in an env file or your environment.`);
|
|
21823
|
+
}
|
|
21824
|
+
return resolved;
|
|
21825
|
+
}
|
|
21826
|
+
// src/core/config.ts
|
|
21752
21827
|
var _globalConnectionName;
|
|
21828
|
+
function assertNoConnectionSelectorOnV1(connectionName) {
|
|
21829
|
+
if (connectionName === undefined)
|
|
21830
|
+
return;
|
|
21831
|
+
throw new ConfigError(`\u9023\u7DDA '${connectionName}' \u4E0D\u5B58\u5728\uFF1A\u6B64\u5C08\u6848\u4F7F\u7528\u55AE\u4E00\u9023\u7DDA (v1) \u8A2D\u5B9A\uFF0C\u6C92\u6709\u5177\u540D\u9023\u7DDA\u3002` + ` \u79FB\u9664 --use / DBCLI_CONNECTION\uFF0C\u6216\u6539\u7528 'dbcli init --conn-name <name>' \u5347\u7D1A\u70BA\u591A\u9023\u7DDA (v2) \u8A2D\u5B9A\u3002`);
|
|
21832
|
+
}
|
|
21753
21833
|
function getGlobalConnectionName() {
|
|
21754
21834
|
return _globalConnectionName;
|
|
21755
21835
|
}
|
|
@@ -21776,18 +21856,10 @@ var DEFAULT_CONFIG = {
|
|
|
21776
21856
|
function isEnvReference(value) {
|
|
21777
21857
|
return typeof value === "object" && value !== null && "$env" in value && typeof value.$env === "string";
|
|
21778
21858
|
}
|
|
21779
|
-
function resolveEnvReferences(config, env, parentKey
|
|
21859
|
+
function resolveEnvReferences(config, env, parentKey) {
|
|
21780
21860
|
if (isEnvReference(config)) {
|
|
21781
21861
|
const envKey = config.$env;
|
|
21782
|
-
const value = env
|
|
21783
|
-
if (!value) {
|
|
21784
|
-
if (!strict) {
|
|
21785
|
-
return config;
|
|
21786
|
-
}
|
|
21787
|
-
throw new ConfigError(`Environment variable not defined: ${envKey}
|
|
21788
|
-
` + `Please set ${envKey} in .env or your environment.
|
|
21789
|
-
` + `Hint: check your .env file or run 'export ${envKey}=<value>'`);
|
|
21790
|
-
}
|
|
21862
|
+
const value = resolveEnvRef(config, parentKey ?? "<root>", env);
|
|
21791
21863
|
if (parentKey === "port") {
|
|
21792
21864
|
const portNum = parseInt(value, 10);
|
|
21793
21865
|
if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
|
|
@@ -21798,12 +21870,12 @@ function resolveEnvReferences(config, env, parentKey, strict = false) {
|
|
|
21798
21870
|
return value;
|
|
21799
21871
|
}
|
|
21800
21872
|
if (Array.isArray(config)) {
|
|
21801
|
-
return config.map((item) => resolveEnvReferences(item, env, parentKey
|
|
21873
|
+
return config.map((item) => resolveEnvReferences(item, env, parentKey));
|
|
21802
21874
|
}
|
|
21803
21875
|
if (typeof config === "object" && config !== null) {
|
|
21804
21876
|
const resolved = {};
|
|
21805
21877
|
for (const [key, value] of Object.entries(config)) {
|
|
21806
|
-
resolved[key] = resolveEnvReferences(value, env, key
|
|
21878
|
+
resolved[key] = resolveEnvReferences(value, env, key);
|
|
21807
21879
|
}
|
|
21808
21880
|
return resolved;
|
|
21809
21881
|
}
|
|
@@ -21853,7 +21925,7 @@ var configModule = {
|
|
|
21853
21925
|
process.env.DBCLI_PASSWORD = legacyPassword;
|
|
21854
21926
|
}
|
|
21855
21927
|
}
|
|
21856
|
-
const resolvedConnection = resolveEnvReferences(resolved.connection, process.env
|
|
21928
|
+
const resolvedConnection = resolveEnvReferences(resolved.connection, process.env);
|
|
21857
21929
|
if (!resolvedConnection.password && legacyPassword) {
|
|
21858
21930
|
resolvedConnection.password = legacyPassword;
|
|
21859
21931
|
}
|
|
@@ -21890,7 +21962,8 @@ var configModule = {
|
|
|
21890
21962
|
redis: v2Config.redis
|
|
21891
21963
|
});
|
|
21892
21964
|
}
|
|
21893
|
-
|
|
21965
|
+
assertNoConnectionSelectorOnV1(effectiveConnectionName);
|
|
21966
|
+
const resolvedConfig = resolveEnvReferences(config, process.env);
|
|
21894
21967
|
const envPath = join9(storagePath, ".env.local");
|
|
21895
21968
|
const envFile = Bun.file(envPath);
|
|
21896
21969
|
if (await envFile.exists()) {
|
|
@@ -21906,9 +21979,10 @@ var configModule = {
|
|
|
21906
21979
|
const file = Bun.file(path);
|
|
21907
21980
|
const exists = await file.exists();
|
|
21908
21981
|
if (exists) {
|
|
21982
|
+
assertNoConnectionSelectorOnV1(effectiveConnectionName);
|
|
21909
21983
|
const content = await file.text();
|
|
21910
21984
|
const raw = JSON.parse(content);
|
|
21911
|
-
const resolved = resolveEnvReferences(raw, process.env
|
|
21985
|
+
const resolved = resolveEnvReferences(raw, process.env);
|
|
21912
21986
|
return DbcliConfigSchema.parse(resolved);
|
|
21913
21987
|
}
|
|
21914
21988
|
return { ...DEFAULT_CONFIG };
|
|
@@ -22016,7 +22090,9 @@ var REDACTED_VALUE_FLAGS = new Set([
|
|
|
22016
22090
|
"--password",
|
|
22017
22091
|
"--token",
|
|
22018
22092
|
"--secret",
|
|
22019
|
-
"--bulk"
|
|
22093
|
+
"--bulk",
|
|
22094
|
+
"--query-file",
|
|
22095
|
+
"-f"
|
|
22020
22096
|
]);
|
|
22021
22097
|
var KEEP_VALUE_FLAGS = new Set([
|
|
22022
22098
|
"--format",
|
|
@@ -22025,17 +22101,33 @@ var KEEP_VALUE_FLAGS = new Set([
|
|
|
22025
22101
|
"--output",
|
|
22026
22102
|
"--limit",
|
|
22027
22103
|
"--collection",
|
|
22028
|
-
"--index"
|
|
22104
|
+
"--index",
|
|
22105
|
+
"--fields",
|
|
22106
|
+
"--truncate"
|
|
22029
22107
|
]);
|
|
22030
22108
|
var LINT_BOOLEAN_FLAGS = new Set(["--no-schema", "--recovery"]);
|
|
22109
|
+
var QUERY_BOOLEAN_FLAGS = new Set(["--ui", "--no-limit", "--no-truncate", "--recovery"]);
|
|
22031
22110
|
function optionParts(token) {
|
|
22111
|
+
if (token.startsWith("-f") && !token.startsWith("--") && token.length > 2) {
|
|
22112
|
+
return { name: "-f", inlineValue: token.slice(2) };
|
|
22113
|
+
}
|
|
22032
22114
|
const equals = token.indexOf("=");
|
|
22033
22115
|
return equals === -1 ? { name: token, inlineValue: undefined } : { name: token.slice(0, equals), inlineValue: token.slice(equals + 1) };
|
|
22034
22116
|
}
|
|
22117
|
+
function isOptionToken(token) {
|
|
22118
|
+
return token.startsWith("--") || token.startsWith("-f");
|
|
22119
|
+
}
|
|
22120
|
+
function isKnownBooleanFlag(command, name) {
|
|
22121
|
+
if (command === "lint")
|
|
22122
|
+
return LINT_BOOLEAN_FLAGS.has(name);
|
|
22123
|
+
if (command === "query")
|
|
22124
|
+
return QUERY_BOOLEAN_FLAGS.has(name);
|
|
22125
|
+
return false;
|
|
22126
|
+
}
|
|
22035
22127
|
function findSensitiveSubcommand(argv) {
|
|
22036
22128
|
for (let index = 1;index < argv.length; index++) {
|
|
22037
22129
|
const token = argv[index];
|
|
22038
|
-
if (token
|
|
22130
|
+
if (isOptionToken(token)) {
|
|
22039
22131
|
const { name, inlineValue } = optionParts(token);
|
|
22040
22132
|
if (inlineValue === undefined && (REDACTED_VALUE_FLAGS.has(name) || KEEP_VALUE_FLAGS.has(name))) {
|
|
22041
22133
|
index++;
|
|
@@ -22058,7 +22150,7 @@ function sensitiveArgvValues(argv) {
|
|
|
22058
22150
|
afterEndOfOptions = true;
|
|
22059
22151
|
continue;
|
|
22060
22152
|
}
|
|
22061
|
-
if (!afterEndOfOptions && token
|
|
22153
|
+
if (!afterEndOfOptions && isOptionToken(token)) {
|
|
22062
22154
|
const { name, inlineValue } = optionParts(token);
|
|
22063
22155
|
if (REDACTED_VALUE_FLAGS.has(name)) {
|
|
22064
22156
|
const value = inlineValue ?? argv[index + 1];
|
|
@@ -22077,7 +22169,7 @@ function sensitiveArgvValues(argv) {
|
|
|
22077
22169
|
index++;
|
|
22078
22170
|
} else if (inlineValue === undefined && KEEP_VALUE_FLAGS.has(name)) {
|
|
22079
22171
|
index++;
|
|
22080
|
-
} else if (sensitiveCommand?.command
|
|
22172
|
+
} else if (isKnownBooleanFlag(sensitiveCommand?.command, name)) {
|
|
22081
22173
|
continue;
|
|
22082
22174
|
} else if (!sensitiveCommand || index < sensitiveCommand.index) {
|
|
22083
22175
|
continue;
|
|
@@ -22108,7 +22200,7 @@ function redactArgv(argv) {
|
|
|
22108
22200
|
afterEndOfOptions = true;
|
|
22109
22201
|
continue;
|
|
22110
22202
|
}
|
|
22111
|
-
if (!afterEndOfOptions && tok
|
|
22203
|
+
if (!afterEndOfOptions && isOptionToken(tok)) {
|
|
22112
22204
|
const { name, inlineValue } = optionParts(tok);
|
|
22113
22205
|
if (REDACTED_VALUE_FLAGS.has(name)) {
|
|
22114
22206
|
out.push(`${name} <redacted>`);
|
|
@@ -22127,7 +22219,7 @@ function redactArgv(argv) {
|
|
|
22127
22219
|
}
|
|
22128
22220
|
continue;
|
|
22129
22221
|
}
|
|
22130
|
-
if (sensitiveCommand?.command
|
|
22222
|
+
if (isKnownBooleanFlag(sensitiveCommand?.command, name)) {
|
|
22131
22223
|
out.push(tok);
|
|
22132
22224
|
continue;
|
|
22133
22225
|
}
|
|
@@ -22163,9 +22255,9 @@ function redactSql(sql) {
|
|
|
22163
22255
|
// src/core/audit/integration-helper.ts
|
|
22164
22256
|
var _sessionIdService = null;
|
|
22165
22257
|
var _loggers = new Map;
|
|
22166
|
-
async function getAuditLogger(config, configPath) {
|
|
22258
|
+
async function getAuditLogger(config, configPath, connectionName) {
|
|
22167
22259
|
const storagePath = await resolveConfigStoragePath(configPath);
|
|
22168
|
-
const connName = config.effectiveConnectionName || getGlobalConnectionName() || "default";
|
|
22260
|
+
const connName = connectionName || config.effectiveConnectionName || getGlobalConnectionName() || "default";
|
|
22169
22261
|
const key = `${storagePath}:${connName}`;
|
|
22170
22262
|
if (!_loggers.has(key)) {
|
|
22171
22263
|
if (!_sessionIdService) {
|
|
@@ -22186,7 +22278,7 @@ async function getAuditLogger(config, configPath) {
|
|
|
22186
22278
|
}
|
|
22187
22279
|
async function writeAuditEntry(config, commandName, options, outcome) {
|
|
22188
22280
|
try {
|
|
22189
|
-
const logger = await getAuditLogger(config, options.config || ".dbcli");
|
|
22281
|
+
const logger = await getAuditLogger(config, options.config || ".dbcli", typeof options.connectionName === "string" ? options.connectionName : undefined);
|
|
22190
22282
|
const engine = config.connection?.system || "postgresql";
|
|
22191
22283
|
const target = outcome.target || getOperationTarget(engine, commandName, options, outcome.sql);
|
|
22192
22284
|
let tier = getEngineCapability(engine, commandName).tier;
|
|
@@ -22218,6 +22310,129 @@ async function writeAuditEntry(config, commandName, options, outcome) {
|
|
|
22218
22310
|
}
|
|
22219
22311
|
}
|
|
22220
22312
|
|
|
22313
|
+
// src/core/field-projection.ts
|
|
22314
|
+
var UNSAFE_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]);
|
|
22315
|
+
function projectRows(rows, selection) {
|
|
22316
|
+
if (selection.mode === "include") {
|
|
22317
|
+
return {
|
|
22318
|
+
rows: rows.map((row) => {
|
|
22319
|
+
const projected = {};
|
|
22320
|
+
for (const path of selection.paths) {
|
|
22321
|
+
const result = readPath(row, path.split("."));
|
|
22322
|
+
defineData(projected, path, result.found ? result.value ?? null : null);
|
|
22323
|
+
}
|
|
22324
|
+
return projected;
|
|
22325
|
+
}),
|
|
22326
|
+
columnNames: [...selection.paths]
|
|
22327
|
+
};
|
|
22328
|
+
}
|
|
22329
|
+
const projectedRows = omitFieldPaths(rows, selection.paths);
|
|
22330
|
+
const columnNames = collectColumnNames(projectedRows);
|
|
22331
|
+
return { rows: normalizeRows(projectedRows, columnNames), columnNames };
|
|
22332
|
+
}
|
|
22333
|
+
function hasFieldPath(row, path) {
|
|
22334
|
+
return readPath(row, path.split(".")).found;
|
|
22335
|
+
}
|
|
22336
|
+
function omitFieldPaths(rows, paths) {
|
|
22337
|
+
return rows.map((row) => {
|
|
22338
|
+
let projected = cloneRecord(row);
|
|
22339
|
+
for (const path of paths)
|
|
22340
|
+
projected = omitPath(projected, path.split("."));
|
|
22341
|
+
return projected;
|
|
22342
|
+
});
|
|
22343
|
+
}
|
|
22344
|
+
function readPath(value, segments) {
|
|
22345
|
+
if (segments.length === 0)
|
|
22346
|
+
return { found: true, value };
|
|
22347
|
+
if (Array.isArray(value)) {
|
|
22348
|
+
const results = value.map((item) => readPath(item, segments));
|
|
22349
|
+
return {
|
|
22350
|
+
found: results.some((result) => result.found),
|
|
22351
|
+
value: results.map((result) => result.found ? result.value : undefined)
|
|
22352
|
+
};
|
|
22353
|
+
}
|
|
22354
|
+
if (!isRecord(value))
|
|
22355
|
+
return { found: false };
|
|
22356
|
+
const exactPath = segments.join(".");
|
|
22357
|
+
if (Object.prototype.hasOwnProperty.call(value, exactPath)) {
|
|
22358
|
+
return { found: true, value: value[exactPath] };
|
|
22359
|
+
}
|
|
22360
|
+
const [head, ...tail] = segments;
|
|
22361
|
+
if (!Object.prototype.hasOwnProperty.call(value, head))
|
|
22362
|
+
return { found: false };
|
|
22363
|
+
return readPath(value[head], tail);
|
|
22364
|
+
}
|
|
22365
|
+
function omitPath(value, segments) {
|
|
22366
|
+
if (segments.length === 0)
|
|
22367
|
+
return value;
|
|
22368
|
+
if (Array.isArray(value))
|
|
22369
|
+
return value.map((item) => omitPath(item, segments));
|
|
22370
|
+
if (!isPlainRecord(value))
|
|
22371
|
+
return value;
|
|
22372
|
+
const exactPath = segments.join(".");
|
|
22373
|
+
const [head, ...tail] = segments;
|
|
22374
|
+
const out = {};
|
|
22375
|
+
for (const [key, child] of Object.entries(value)) {
|
|
22376
|
+
if (key === exactPath)
|
|
22377
|
+
continue;
|
|
22378
|
+
if (key === head) {
|
|
22379
|
+
if (tail.length > 0)
|
|
22380
|
+
defineData(out, key, omitPath(child, tail));
|
|
22381
|
+
continue;
|
|
22382
|
+
}
|
|
22383
|
+
defineData(out, key, child);
|
|
22384
|
+
}
|
|
22385
|
+
return out;
|
|
22386
|
+
}
|
|
22387
|
+
function cloneRecord(value, omittedKey) {
|
|
22388
|
+
const out = {};
|
|
22389
|
+
for (const [key, child] of Object.entries(value)) {
|
|
22390
|
+
if (key !== omittedKey)
|
|
22391
|
+
defineData(out, key, child);
|
|
22392
|
+
}
|
|
22393
|
+
return out;
|
|
22394
|
+
}
|
|
22395
|
+
function collectColumnNames(rows) {
|
|
22396
|
+
const names = [];
|
|
22397
|
+
const seen = new Set;
|
|
22398
|
+
for (const row of rows) {
|
|
22399
|
+
for (const name of Object.keys(row)) {
|
|
22400
|
+
if (!seen.has(name)) {
|
|
22401
|
+
seen.add(name);
|
|
22402
|
+
names.push(name);
|
|
22403
|
+
}
|
|
22404
|
+
}
|
|
22405
|
+
}
|
|
22406
|
+
return names;
|
|
22407
|
+
}
|
|
22408
|
+
function normalizeRows(rows, columnNames) {
|
|
22409
|
+
return rows.map((row) => {
|
|
22410
|
+
const normalized = {};
|
|
22411
|
+
for (const column of columnNames) {
|
|
22412
|
+
const value = Object.prototype.hasOwnProperty.call(row, column) ? row[column] : null;
|
|
22413
|
+
defineData(normalized, column, value ?? null);
|
|
22414
|
+
}
|
|
22415
|
+
return normalized;
|
|
22416
|
+
});
|
|
22417
|
+
}
|
|
22418
|
+
function isRecord(value) {
|
|
22419
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
22420
|
+
}
|
|
22421
|
+
function isPlainRecord(value) {
|
|
22422
|
+
if (!isRecord(value))
|
|
22423
|
+
return false;
|
|
22424
|
+
const prototype = Object.getPrototypeOf(value);
|
|
22425
|
+
return prototype === Object.prototype || prototype === null;
|
|
22426
|
+
}
|
|
22427
|
+
function defineData(target, key, value) {
|
|
22428
|
+
Object.defineProperty(target, key, {
|
|
22429
|
+
value,
|
|
22430
|
+
enumerable: true,
|
|
22431
|
+
configurable: true,
|
|
22432
|
+
writable: true
|
|
22433
|
+
});
|
|
22434
|
+
}
|
|
22435
|
+
|
|
22221
22436
|
// src/core/query-executor.ts
|
|
22222
22437
|
class QueryExecutor {
|
|
22223
22438
|
adapter;
|
|
@@ -22225,6 +22440,7 @@ class QueryExecutor {
|
|
|
22225
22440
|
blacklistValidator;
|
|
22226
22441
|
config;
|
|
22227
22442
|
options;
|
|
22443
|
+
pendingDiagnostics = [];
|
|
22228
22444
|
constructor(adapter, permission, blacklistValidator, config, options = {}) {
|
|
22229
22445
|
this.adapter = adapter;
|
|
22230
22446
|
this.permission = permission;
|
|
@@ -22232,19 +22448,31 @@ class QueryExecutor {
|
|
|
22232
22448
|
this.config = config;
|
|
22233
22449
|
this.options = options;
|
|
22234
22450
|
}
|
|
22451
|
+
takeDiagnostics() {
|
|
22452
|
+
const diagnostics = this.pendingDiagnostics;
|
|
22453
|
+
this.pendingDiagnostics = [];
|
|
22454
|
+
return diagnostics;
|
|
22455
|
+
}
|
|
22235
22456
|
async execute(sql, options) {
|
|
22236
22457
|
const start = performance.now();
|
|
22458
|
+
this.pendingDiagnostics = [];
|
|
22237
22459
|
try {
|
|
22238
22460
|
const classification = enforcePermission(sql, this.permission);
|
|
22239
|
-
|
|
22240
|
-
console.error(`\u26A0 Warning: executing ${classification.type} operation (admin mode)`);
|
|
22241
|
-
}
|
|
22461
|
+
const dangerousOperationWarning = classification.isDangerous && this.permission === "admin" ? `\u26A0 Warning: executing ${classification.type} operation (admin mode)` : undefined;
|
|
22242
22462
|
const AUTO_LIMIT_TYPES = new Set(["SELECT"]);
|
|
22243
22463
|
let executeSql = sql;
|
|
22244
|
-
|
|
22245
|
-
|
|
22246
|
-
|
|
22247
|
-
|
|
22464
|
+
let appliedLimit;
|
|
22465
|
+
let autoLimitWarning;
|
|
22466
|
+
if (AUTO_LIMIT_TYPES.has(classification.type) && !hasUserAuthoredLimit(executeSql) && options?.autoLimit !== false) {
|
|
22467
|
+
const requestedLimit = options?.limitValue ?? (this.permission === "query-only" ? DEFAULT_QUERY_ONLY_LIMIT : undefined);
|
|
22468
|
+
if (requestedLimit !== undefined) {
|
|
22469
|
+
const fetchLimit = requestedLimit + (options?.detectTruncation === true ? 1 : 0);
|
|
22470
|
+
appliedLimit = options?.detectTruncation === true ? requestedLimit : undefined;
|
|
22471
|
+
executeSql = `${executeSql.replace(/;\s*$/, "")} LIMIT ${fetchLimit}`;
|
|
22472
|
+
if (options?.limitValue === undefined && this.permission === "query-only") {
|
|
22473
|
+
autoLimitWarning = `Query-only mode: auto-limiting to ${requestedLimit} rows`;
|
|
22474
|
+
}
|
|
22475
|
+
}
|
|
22248
22476
|
}
|
|
22249
22477
|
if (this.blacklistValidator) {
|
|
22250
22478
|
const tableName = extractTableName(sql);
|
|
@@ -22254,26 +22482,39 @@ class QueryExecutor {
|
|
|
22254
22482
|
}
|
|
22255
22483
|
const resultData = await this.adapter.execute(executeSql);
|
|
22256
22484
|
const executionTimeMs = Math.round(performance.now() - start);
|
|
22257
|
-
const
|
|
22485
|
+
const limitedResult = appliedLimit === undefined ? undefined : trimAppliedLimit(resultData.rows, appliedLimit);
|
|
22486
|
+
const rows = limitedResult?.rows ?? resultData.rows;
|
|
22258
22487
|
const affectedRows = resultData.affectedRows;
|
|
22488
|
+
const visibleAffectedRows = limitedResult ? rows.length : affectedRows;
|
|
22259
22489
|
let columnNames = rows.length > 0 && rows[0] ? Object.keys(rows[0]) : [];
|
|
22260
|
-
const columnTypes = columnNames.map((col) => {
|
|
22261
|
-
const value = rows[0]?.[col];
|
|
22262
|
-
return inferColumnType(value);
|
|
22263
|
-
});
|
|
22264
22490
|
let filteredRows = rows;
|
|
22265
22491
|
let securityNotification;
|
|
22492
|
+
let omittedColumns = [];
|
|
22266
22493
|
if (this.blacklistValidator) {
|
|
22267
22494
|
const tableName = extractTableName(sql);
|
|
22268
22495
|
if (tableName) {
|
|
22269
22496
|
const filterResult = this.blacklistValidator.filterColumns(tableName, rows, columnNames);
|
|
22270
22497
|
filteredRows = filterResult.filteredRows;
|
|
22271
22498
|
if (filterResult.omittedColumns.length > 0) {
|
|
22499
|
+
omittedColumns = filterResult.omittedColumns;
|
|
22272
22500
|
columnNames = columnNames.filter((col) => !filterResult.omittedColumns.includes(col));
|
|
22273
22501
|
securityNotification = this.blacklistValidator.buildSecurityNotification(tableName, filterResult.omittedColumns);
|
|
22274
22502
|
}
|
|
22275
22503
|
}
|
|
22276
22504
|
}
|
|
22505
|
+
if (options?.fieldSelection) {
|
|
22506
|
+
const fieldSelection = options.fieldSelection.mode === "include" ? {
|
|
22507
|
+
mode: "include",
|
|
22508
|
+
paths: options.fieldSelection.paths.filter((path) => !omittedColumns.some((omitted) => path === omitted || path.startsWith(`${omitted}.`)))
|
|
22509
|
+
} : options.fieldSelection;
|
|
22510
|
+
const projection = projectRows(filteredRows, fieldSelection);
|
|
22511
|
+
filteredRows = projection.rows;
|
|
22512
|
+
columnNames = projection.columnNames;
|
|
22513
|
+
}
|
|
22514
|
+
const columnTypes = columnNames.map((column) => {
|
|
22515
|
+
const value = filteredRows.find((row) => row[column] !== undefined)?.[column];
|
|
22516
|
+
return inferColumnType(value);
|
|
22517
|
+
});
|
|
22277
22518
|
const result = {
|
|
22278
22519
|
rows: filteredRows,
|
|
22279
22520
|
rowCount: filteredRows.length,
|
|
@@ -22282,20 +22523,30 @@ class QueryExecutor {
|
|
|
22282
22523
|
executionTimeMs,
|
|
22283
22524
|
metadata: {
|
|
22284
22525
|
statement: classification.type,
|
|
22285
|
-
affectedRows,
|
|
22526
|
+
affectedRows: visibleAffectedRows,
|
|
22286
22527
|
...securityNotification ? { securityNotification } : {}
|
|
22287
|
-
}
|
|
22528
|
+
},
|
|
22529
|
+
...limitedResult ? { appliedLimit: limitedResult.metadata } : {}
|
|
22288
22530
|
};
|
|
22289
22531
|
if (this.config) {
|
|
22290
22532
|
await writeAuditEntry(this.config, "query", this.options, {
|
|
22291
22533
|
success: true,
|
|
22292
22534
|
sql,
|
|
22293
22535
|
metadata: {
|
|
22294
|
-
rows_affected:
|
|
22536
|
+
rows_affected: visibleAffectedRows,
|
|
22295
22537
|
execution_ms: executionTimeMs
|
|
22296
22538
|
}
|
|
22297
22539
|
});
|
|
22298
22540
|
}
|
|
22541
|
+
if (this.options.recovery !== true) {
|
|
22542
|
+
const diagnostics = [dangerousOperationWarning, autoLimitWarning].filter((diagnostic) => diagnostic !== undefined);
|
|
22543
|
+
if (this.options.deferDiagnostics === true) {
|
|
22544
|
+
this.pendingDiagnostics = diagnostics;
|
|
22545
|
+
} else {
|
|
22546
|
+
for (const diagnostic of diagnostics)
|
|
22547
|
+
console.error(diagnostic);
|
|
22548
|
+
}
|
|
22549
|
+
}
|
|
22299
22550
|
return result;
|
|
22300
22551
|
} catch (error) {
|
|
22301
22552
|
if (this.config) {
|
|
@@ -22329,6 +22580,10 @@ class QueryExecutor {
|
|
|
22329
22580
|
}
|
|
22330
22581
|
}
|
|
22331
22582
|
}
|
|
22583
|
+
function hasUserAuthoredLimit(sql) {
|
|
22584
|
+
const executableSql = stripCommentsAndStrings(sql).replace(/`(?:``|[^`])*`/g, " ");
|
|
22585
|
+
return /\bLIMIT\s+(?:\(\s*)?(?:\d+|ALL\b|\?|\$\d+|:[A-Za-z_][A-Za-z0-9_]*)/i.test(executableSql);
|
|
22586
|
+
}
|
|
22332
22587
|
function inferColumnType(value) {
|
|
22333
22588
|
if (value === null || value === undefined) {
|
|
22334
22589
|
return "null";
|
|
@@ -22964,19 +23219,11 @@ class BlacklistValidator {
|
|
|
22964
23219
|
if (blacklistedColumns.length === 0) {
|
|
22965
23220
|
return { filteredRows: rows, omittedColumns: [] };
|
|
22966
23221
|
}
|
|
22967
|
-
const omittedColumns =
|
|
23222
|
+
const omittedColumns = blacklistedColumns.filter((path3) => columnList.includes(path3) || rows.some((row) => hasFieldPath(row, path3)));
|
|
22968
23223
|
if (omittedColumns.length === 0) {
|
|
22969
23224
|
return { filteredRows: rows, omittedColumns: [] };
|
|
22970
23225
|
}
|
|
22971
|
-
const filteredRows = rows
|
|
22972
|
-
const newRow = {};
|
|
22973
|
-
for (const [key, value] of Object.entries(row)) {
|
|
22974
|
-
if (!omittedColumns.includes(key)) {
|
|
22975
|
-
newRow[key] = value;
|
|
22976
|
-
}
|
|
22977
|
-
}
|
|
22978
|
-
return newRow;
|
|
22979
|
-
});
|
|
23226
|
+
const filteredRows = omitFieldPaths(rows, omittedColumns);
|
|
22980
23227
|
return { filteredRows, omittedColumns };
|
|
22981
23228
|
}
|
|
22982
23229
|
buildSecurityNotification(_tableName, omittedColumns) {
|