fix(redis): skip confirmation for regular writes
This commit is contained in:
1 parent
8c8c48f9b1
commit
39d956f2e8
20 files changed
+283
-84
No files matched your search
@@ -151,6 +151,11 @@ const dangerConfirmLabel = computed(() => {
|
||||
if (pendingDanger.value?.kind === "command") return t("dangerDialog.confirm");
|
||||
return t("dangerDialog.deleteConfirm");
|
||||
});
|
||||
const dangerMessage = computed(() => {
|
||||
// Redis write commands such as SET/HSET are mutating but not necessarily delete operations.
|
||||
if (pendingDanger.value?.kind === "command") return t("dangerDialog.redisCommandMessage");
|
||||
return t("dangerDialog.deleteMessage");
|
||||
});
|
||||
const commandPrompt = computed(() => `db${commandDb.value}>`);
|
||||
const createKeyTypeOptions = computed<{ value: RedisCreateKeyType; label: string }[]>(() => [
|
||||
{ value: "string", label: "String" },
|
||||
@@ -518,12 +523,11 @@ async function runRedisCommand(command: string) {
|
||||
// The db this command ran on — capture before nextRedisCommandDb() advances it.
|
||||
const executedDb = commandDb.value;
|
||||
commandDb.value = nextRedisCommandDb(commandDb.value, command, result.value);
|
||||
if (result.safety === "confirm") {
|
||||
await loadKeys();
|
||||
}
|
||||
// Drop the cached key-name completion for this db so the editor's autocomplete
|
||||
// reflects keys added/removed/renamed by SET/DEL/RENAME/...
|
||||
if (isRedisMutatingCommand(command)) {
|
||||
const mutatesKeys = isRedisMutatingCommand(command);
|
||||
if (mutatesKeys) {
|
||||
await loadKeys();
|
||||
connectionStore.invalidateCompletionCache(props.connectionId, String(executedDb));
|
||||
// Refresh the sidebar db key counts (INFO keyspace) so `dbN (count)` stays accurate
|
||||
// after the write. Fire-and-forget so the terminal stays responsive.
|
||||
@@ -1224,7 +1228,7 @@ defineExpose({ focusSearch });
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
|
||||
<DangerConfirmDialog v-model:open="showDangerConfirm" :message="t('dangerDialog.deleteMessage')" :details="dangerDetails" :confirm-label="dangerConfirmLabel" @confirm="applyDangerAction" />
|
||||
<DangerConfirmDialog v-model:open="showDangerConfirm" :message="dangerMessage" :details="dangerDetails" :confirm-label="dangerConfirmLabel" @confirm="applyDangerAction" />
|
||||
|
||||
<Dialog v-model:open="showCreateKeyDialog">
|
||||
<DialogContent class="sm:max-w-md" :style="editorFontFamilyStyle">
|
||||
|
||||
@@ -2223,6 +2223,7 @@ export default {
|
||||
dangerDialog: {
|
||||
title: "Dangerous Operation",
|
||||
message: "This SQL statement may modify or delete data irreversibly. Are you sure you want to execute it?",
|
||||
redisCommandMessage: "This Redis command may modify data and cannot be undone automatically. Continue?",
|
||||
suppressFuturePrompts: "Do not ask again for dangerous SQL",
|
||||
wrapLines: "Toggle word wrap",
|
||||
deleteMessage: "This delete operation may be irreversible. Continue?",
|
||||
|
||||
@@ -2155,6 +2155,7 @@ export default withEnglishFallback({
|
||||
dangerDialog: {
|
||||
title: "Operación peligrosa",
|
||||
message: "Esta sentencia SQL puede modificar o eliminar datos de forma irreversible. ¿Estás seguro de que deseas ejecutarla?",
|
||||
redisCommandMessage: "Este comando de Redis puede modificar datos y no se puede deshacer automáticamente. ¿Continuar?",
|
||||
suppressFuturePrompts: "No volver a preguntar para SQL peligroso",
|
||||
wrapLines: "Alternar ajuste de línea",
|
||||
deleteMessage: "Esta operación de eliminación puede ser irreversible. ¿Continuar?",
|
||||
|
||||
@@ -2153,6 +2153,7 @@ export default withEnglishFallback({
|
||||
dangerDialog: {
|
||||
title: "Operazione Pericolosa",
|
||||
message: "Questa istruzione SQL potrebbe modificare o eliminare i dati in modo irreversibile. Sei sicuro di volerla eseguire?",
|
||||
redisCommandMessage: "Questo comando Redis può modificare i dati e non può essere annullato automaticamente. Continuare?",
|
||||
suppressFuturePrompts: "Non chiedere più per SQL pericolosi",
|
||||
wrapLines: "Attiva/disattiva ritorno a capo",
|
||||
deleteMessage: "Questa operazione di eliminazione potrebbe essere irreversibile. Continuare?",
|
||||
|
||||
@@ -2153,6 +2153,7 @@ export default withEnglishFallback({
|
||||
dangerDialog: {
|
||||
title: "危険な操作",
|
||||
message: "このSQL文はデータを不可逆的に変更または削除する可能性があります。実行してもよろしいですか?",
|
||||
redisCommandMessage: "このRedisコマンドはデータを変更し、自動的に元に戻せない可能性があります。続行しますか?",
|
||||
suppressFuturePrompts: "危険なSQLの確認を今後表示しない",
|
||||
wrapLines: "折り返し表示を切り替え",
|
||||
deleteMessage: "この削除操作は元に戻せない可能性があります。続行しますか?",
|
||||
|
||||
@@ -2154,6 +2154,7 @@ export default withEnglishFallback({
|
||||
dangerDialog: {
|
||||
title: "Operação perigosa",
|
||||
message: "Esta instrução SQL pode modificar ou excluir dados de forma irreversível. Tem certeza de que deseja executá-la?",
|
||||
redisCommandMessage: "Este comando Redis pode modificar dados e não pode ser desfeito automaticamente. Continuar?",
|
||||
suppressFuturePrompts: "Não perguntar novamente para SQL perigoso",
|
||||
wrapLines: "Alternar quebra de linha",
|
||||
deleteMessage: "Esta operação de exclusão pode ser irreversível. Continuar?",
|
||||
|
||||
@@ -2223,6 +2223,7 @@ export default withEnglishFallback({
|
||||
dangerDialog: {
|
||||
title: "危险操作",
|
||||
message: "此 SQL 语句可能不可逆地修改或删除数据,确认要执行吗?",
|
||||
redisCommandMessage: "此 Redis 命令可能会修改数据,且无法自动撤销,确认要继续吗?",
|
||||
suppressFuturePrompts: "以后执行危险 SQL 不再提示",
|
||||
wrapLines: "切换自动换行",
|
||||
deleteMessage: "此删除操作可能不可逆,确认要继续吗?",
|
||||
|
||||
@@ -2010,6 +2010,7 @@ export default withEnglishFallback({
|
||||
dangerDialog: {
|
||||
title: "危險操作",
|
||||
message: "此 SQL 語句可能不可逆地修改或刪除資料,確認要執行嗎?",
|
||||
redisCommandMessage: "此 Redis 命令可能會修改資料,且無法自動復原,確認要繼續嗎?",
|
||||
suppressFuturePrompts: "之後執行危險 SQL 不再提示",
|
||||
wrapLines: "切換自動換行",
|
||||
deleteMessage: "此刪除操作可能不可逆,確認要繼續嗎?",
|
||||
|
||||
@@ -1391,7 +1391,7 @@ export interface RedisScanResult {
|
||||
total_keys: number;
|
||||
}
|
||||
|
||||
export type RedisCommandSafety = "allowed" | "confirm" | "blocked";
|
||||
export type RedisCommandSafety = "allowed" | "write" | "confirm" | "blocked";
|
||||
|
||||
export interface RedisCommandResult {
|
||||
command: string;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type RedisCommandSafety = "allowed" | "confirm" | "blocked";
|
||||
export type RedisCommandSafety = "allowed" | "write" | "confirm" | "blocked";
|
||||
|
||||
const BLOCKED_COMMANDS = new Set(["KEYS", "FLUSHALL", "SHUTDOWN", "CONFIG", "SAVE", "BGSAVE", "SLAVEOF", "REPLICAOF", "MIGRATE", "MODULE", "SCRIPT", "EVAL", "EVALSHA"]);
|
||||
|
||||
@@ -9,30 +9,84 @@ const CONFIRM_COMMANDS = new Set([
|
||||
"EXPIREAT",
|
||||
"PEXPIRE",
|
||||
"PEXPIREAT",
|
||||
"PERSIST",
|
||||
"RENAME",
|
||||
"RENAMENX",
|
||||
"GETDEL",
|
||||
"HDEL",
|
||||
"LPOP",
|
||||
"RPOP",
|
||||
"LREM",
|
||||
"LTRIM",
|
||||
"SPOP",
|
||||
"SREM",
|
||||
"ZREM",
|
||||
"ZPOPMAX",
|
||||
"ZPOPMIN",
|
||||
"ZMPOP",
|
||||
"ZREMRANGEBYLEX",
|
||||
"ZREMRANGEBYRANK",
|
||||
"ZREMRANGEBYSCORE",
|
||||
"XDEL",
|
||||
"XTRIM",
|
||||
"MOVE",
|
||||
"SORT",
|
||||
"SDIFFSTORE",
|
||||
"SINTERSTORE",
|
||||
"SUNIONSTORE",
|
||||
"ZDIFFSTORE",
|
||||
"ZINTERSTORE",
|
||||
"ZRANGESTORE",
|
||||
"ZUNIONSTORE",
|
||||
"PFMERGE",
|
||||
"GEOSEARCHSTORE",
|
||||
"FLUSHDB",
|
||||
]);
|
||||
|
||||
const WRITE_COMMANDS = new Set([
|
||||
"APPEND",
|
||||
"BITFIELD",
|
||||
"BITOP",
|
||||
"COPY",
|
||||
"DECR",
|
||||
"DECRBY",
|
||||
"GEOADD",
|
||||
"GEORADIUS",
|
||||
"GEORADIUSBYMEMBER",
|
||||
"GETSET",
|
||||
"INCR",
|
||||
"INCRBY",
|
||||
"INCRBYFLOAT",
|
||||
"SET",
|
||||
"SETEX",
|
||||
"PSETEX",
|
||||
"SETNX",
|
||||
"SETRANGE",
|
||||
"MSET",
|
||||
"MSETNX",
|
||||
"PERSIST",
|
||||
"HSET",
|
||||
"HDEL",
|
||||
"LPUSH",
|
||||
"RPUSH",
|
||||
"LPOP",
|
||||
"RPOP",
|
||||
"HMSET",
|
||||
"HINCRBY",
|
||||
"HINCRBYFLOAT",
|
||||
"HSETNX",
|
||||
"LINSERT",
|
||||
"LSET",
|
||||
"LREM",
|
||||
"LMOVE",
|
||||
"LPUSH",
|
||||
"LPUSHX",
|
||||
"PFADD",
|
||||
"RPUSH",
|
||||
"RPUSHX",
|
||||
"RESTORE",
|
||||
"SADD",
|
||||
"SREM",
|
||||
"ZADD",
|
||||
"ZREM",
|
||||
"ZINCRBY",
|
||||
"SETBIT",
|
||||
"XADD",
|
||||
"XDEL",
|
||||
"FLUSHDB",
|
||||
"XACK",
|
||||
"XAUTOCLAIM",
|
||||
"XCLAIM",
|
||||
"XSETID",
|
||||
]);
|
||||
|
||||
export function firstRedisCommandToken(command: string): string {
|
||||
@@ -64,5 +118,6 @@ export function classifyRedisCommandSafety(command: string): RedisCommandSafety
|
||||
const token = firstRedisCommandToken(command);
|
||||
if (BLOCKED_COMMANDS.has(token)) return "blocked";
|
||||
if (CONFIRM_COMMANDS.has(token)) return "confirm";
|
||||
if (WRITE_COMMANDS.has(token)) return "write";
|
||||
return "allowed";
|
||||
}
|
||||
@@ -2,15 +2,17 @@
|
||||
* Static Redis command metadata, distilled from the official Redis command table
|
||||
* (redis-doc `commands.json` / the `COMMAND` output). Kept offline and lightweight:
|
||||
* we only retain what the editor syntax diagnostics need — `arity` and `group` —
|
||||
* plus a `safety` hint for danger/write-command highlighting.
|
||||
* plus a `safety` hint for command gating and write-command highlighting.
|
||||
*
|
||||
* Arity semantics (matches the Redis `COMMAND` reply, command name INCLUDED in the count):
|
||||
* arity > 0 → the command takes exactly `arity` tokens (e.g. GET key → arity 2).
|
||||
* arity < 0 → the command takes AT LEAST `-arity` tokens (e.g. MSET k v [k v ...] → arity -3).
|
||||
*
|
||||
* `safety` is aligned with the token-level sets in `redisCommandSafety.ts` (used to gate
|
||||
* execution). Here it is recorded per-command (including subcommands) so diagnostics can
|
||||
* be more precise than the first-token classification.
|
||||
* execution). `write` commands mutate data without a confirmation prompt; `confirm`
|
||||
* commands are destructive/structural enough to ask first. Here it is recorded
|
||||
* per-command (including subcommands) so diagnostics can be more precise than the
|
||||
* first-token classification.
|
||||
*
|
||||
* Subcommands are keyed as `"MAIN SUB"` in UPPER CASE (e.g. `"CONFIG GET"`, `"XGROUP CREATE"`).
|
||||
*/
|
||||
@@ -27,6 +29,57 @@ export interface RedisCommandSpec {
|
||||
|
||||
type Spec = [arity: number, group: string, safety?: RedisCommandSafety];
|
||||
|
||||
const WRITE_WITHOUT_CONFIRM = new Set([
|
||||
"APPEND",
|
||||
"BITFIELD",
|
||||
"BITOP",
|
||||
"CLIENT SETINFO",
|
||||
"CLIENT SETNAME",
|
||||
"COPY",
|
||||
"DECR",
|
||||
"DECRBY",
|
||||
"GEOADD",
|
||||
"GEORADIUS",
|
||||
"GEORADIUSBYMEMBER",
|
||||
"GETSET",
|
||||
"HINCRBY",
|
||||
"HINCRBYFLOAT",
|
||||
"HMSET",
|
||||
"HSET",
|
||||
"HSETNX",
|
||||
"INCR",
|
||||
"INCRBY",
|
||||
"INCRBYFLOAT",
|
||||
"LINSERT",
|
||||
"LMOVE",
|
||||
"LPUSH",
|
||||
"LPUSHX",
|
||||
"MSET",
|
||||
"MSETNX",
|
||||
"PERSIST",
|
||||
"PFADD",
|
||||
"PSETEX",
|
||||
"RESTORE",
|
||||
"RPUSH",
|
||||
"RPUSHX",
|
||||
"SADD",
|
||||
"SET",
|
||||
"SETBIT",
|
||||
"SETEX",
|
||||
"SETNX",
|
||||
"SETRANGE",
|
||||
"XACK",
|
||||
"XADD",
|
||||
"XAUTOCLAIM",
|
||||
"XCLAIM",
|
||||
"XGROUP CREATE",
|
||||
"XGROUP CREATECONSUMER",
|
||||
"XGROUP SETID",
|
||||
"XSETID",
|
||||
"ZADD",
|
||||
"ZINCRBY",
|
||||
]);
|
||||
|
||||
// Compact tuple form → expanded into the record below.
|
||||
const RAW_COMMANDS: Record<string, Spec> = {
|
||||
// ---- String ----
|
||||
@@ -367,7 +420,12 @@ const RAW_COMMANDS: Record<string, Spec> = {
|
||||
READWRITE: [1, "cluster"],
|
||||
};
|
||||
|
||||
export const REDIS_COMMAND_TABLE: Record<string, RedisCommandSpec> = Object.fromEntries(Object.entries(RAW_COMMANDS).map(([name, [arity, group, safety]]) => [name.toUpperCase(), { arity, group, safety: safety ?? "allowed" }]));
|
||||
function commandTableSafety(name: string, safety?: RedisCommandSafety): RedisCommandSafety {
|
||||
if (safety === "confirm" && WRITE_WITHOUT_CONFIRM.has(name.toUpperCase())) return "write";
|
||||
return safety ?? "allowed";
|
||||
}
|
||||
|
||||
export const REDIS_COMMAND_TABLE: Record<string, RedisCommandSpec> = Object.fromEntries(Object.entries(RAW_COMMANDS).map(([name, [arity, group, safety]]) => [name.toUpperCase(), { arity, group, safety: commandTableSafety(name, safety) }]));
|
||||
|
||||
/**
|
||||
* Resolve a command spec. Handles two-token subcommands (e.g. `XGROUP CREATE`,
|
||||
@@ -442,7 +500,7 @@ const NON_MUTATING_BLOCKED = new Set(["KEYS", "BGSAVE", "SAVE", "SHUTDOWN", "REP
|
||||
* completion for that db is potentially stale and should be dropped.
|
||||
*
|
||||
* Mapping from the diagnostic `safety` field:
|
||||
* - `confirm` → every write command mutates keys (SET/DEL/INCR/HSET/...).
|
||||
* - `write`/`confirm` → every write command mutates keys (SET/DEL/INCR/HSET/...).
|
||||
* - `blocked` → mostly destructive/admin; we keep the ones that may touch keys
|
||||
* (FLUSHALL, MIGRATE, EVAL[ESHA]) and exclude the read-only/admin
|
||||
* ones in `NON_MUTATING_BLOCKED`.
|
||||
@@ -455,7 +513,7 @@ export function isRedisMutatingCommand(command: string): boolean {
|
||||
const argv = firstRedisArgvUpper(command);
|
||||
const spec = resolveRedisCommandSpec(argv);
|
||||
if (!spec) return false;
|
||||
if (spec.safety === "confirm") return true;
|
||||
if (spec.safety === "write" || spec.safety === "confirm") return true;
|
||||
if (spec.safety === "blocked") {
|
||||
return !NON_MUTATING_BLOCKED.has(argv[0]);
|
||||
}
|
||||
|
||||
@@ -196,7 +196,7 @@ export function buildRedisSyntaxDiagnostics(source: string): SqlSemanticDiagnost
|
||||
} else if (spec.safety === "confirm") {
|
||||
diagnostics.push({
|
||||
span: lineSpan(lineNo, commandToken.startColumn, commandToken.endColumn),
|
||||
message: `Write command '${argv[0].value}' — will modify data`,
|
||||
message: `Dangerous command '${argv[0].value}' — confirmation recommended`,
|
||||
severity: "warning",
|
||||
});
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user