Editor settings, SSH/SSL runtime, data import + table-menu loose ends (#7)
* feat: editor settings model + typed clamped defaults (Task 1) * feat: carry SSH config + ssh_password to backend DbConfig (Task 2) * feat: Change enum bulk/drop/empty + type-specific change payload builder (Task 3) * feat: csv parser + shared export util (Task 4) * feat: rustls TLS connector factory with modes + client auth (Task 5) * feat: real SSH tunnel manager (testable backend) + pool eviction hook (Task 6) * feat: table DDL fetch (sqlite + pg_dump arg builder) (Task 7) * feat: keychain SSH secrets + connection delete purge + tunnel lifecycle (Task 8) * feat: real SSH tunnel + TLS connect path for postgres/mysql (Task 9) * feat: execute_change bulk/drop/empty + get_table_ddl command (Task 10) * feat: fetch SSH secrets into dbConnect + save on connection form (Task 11) * feat: Editor settings tab UI (Task 12) * feat: QueryEditor applies editor settings live (Task 13) * feat: ImportDialog with CSV/JSON preview + column mapping (Task 14) * feat: table-menu export/empty/delete/import + queue labels + payload builder (Task 15) * fix: error sanitization, encrypted-key guard, row-indexed import errors, caps (Task 16) * docs: mark Editor Settings, SSH/SSL runtime, Data Import, table-menu loose ends shipped (Task 17) * feat: auto-refresh schema tree after schema-modifying SQL (query + queue drop) * fix: use theme-consistent red classes for danger menu items (text-error was undefined) * feat: changes queue as tab-bar popover + amber pending border * feat: redesign changes popover (visual/SQL toggle, cards, footer actions, Cmd+S) * refactor: drop per-card status label from changes popover cards * feat: green completion indicator on committed cards + auto-close tabs of dropped tables * docs: changes queue popover UX + auto schema refresh statuses
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
import type { QueueItem } from "../stores/dbViewerStore";
|
||||
|
||||
/**
|
||||
* Payload emitted for a single queued change, keyed to match the Rust
|
||||
* `Change` enum variants (snake_case). Sent to `executeChange`.
|
||||
*/
|
||||
export type ChangePayload = Record<string, unknown>;
|
||||
|
||||
function j(v: unknown): string {
|
||||
return JSON.stringify(v ?? {});
|
||||
}
|
||||
|
||||
function q(s: string): string {
|
||||
return `"${s.replace(/"/g, '""')}"`;
|
||||
}
|
||||
|
||||
function lit(v: unknown): string {
|
||||
if (v === null || v === undefined) return "NULL";
|
||||
if (typeof v === "boolean") return v ? "TRUE" : "FALSE";
|
||||
if (typeof v === "number") return String(v);
|
||||
if (typeof v === "string") return `'${v.replace(/'/g, "''")}'`;
|
||||
return `'${JSON.stringify(v).replace(/'/g, "''")}'`;
|
||||
}
|
||||
|
||||
function tableRef(schema: string | undefined, table: string | undefined): string {
|
||||
if (!table) return "-";
|
||||
return schema ? `${q(schema)}.${q(table)}` : q(table);
|
||||
}
|
||||
|
||||
export function buildChangeSql(item: QueueItem): string {
|
||||
const t = tableRef(item.schema, item.table);
|
||||
switch (item.type) {
|
||||
case "insert": {
|
||||
const data = item.newData ?? {};
|
||||
const cols = Object.keys(data);
|
||||
return `INSERT INTO ${t} (${cols.map(q).join(", ")}) VALUES (${cols.map((c) => lit(data[c])).join(", ")})`;
|
||||
}
|
||||
case "update": {
|
||||
const data = item.newData ?? {};
|
||||
const pk = item.primaryKey ?? {};
|
||||
const setClause = Object.keys(data).map((c) => `${q(c)} = ${lit(data[c])}`).join(", ");
|
||||
const whereClause = Object.keys(pk).map((c) => `${q(c)} = ${lit(pk[c])}`).join(" AND ");
|
||||
return `UPDATE ${t} SET ${setClause} WHERE ${whereClause}`;
|
||||
}
|
||||
case "delete": {
|
||||
const pk = item.primaryKey ?? {};
|
||||
const whereClause = Object.keys(pk).map((c) => `${q(c)} = ${lit(pk[c])}`).join(" AND ");
|
||||
return `DELETE FROM ${t} WHERE ${whereClause}`;
|
||||
}
|
||||
case "bulk_insert": {
|
||||
const cols = item.columns ?? [];
|
||||
const rows = item.rows ?? [];
|
||||
const valueRows = rows
|
||||
.map((row) => `(${row.map(lit).join(", ")})`)
|
||||
.join(", ");
|
||||
return `INSERT INTO ${t} (${cols.map(q).join(", ")}) VALUES ${valueRows}`;
|
||||
}
|
||||
case "empty_table":
|
||||
return `DELETE FROM ${t}`;
|
||||
case "drop_table":
|
||||
return `DROP TABLE ${t}`;
|
||||
default:
|
||||
return item.sql ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
export function buildChangePayload(item: QueueItem): ChangePayload {
|
||||
const schema = item.schema ?? "";
|
||||
const table = item.table ?? "";
|
||||
switch (item.type) {
|
||||
case "insert":
|
||||
return { id: item.id, type: "insert", schema, table, data: j(item.newData) };
|
||||
case "update":
|
||||
return { id: item.id, type: "update", schema, table,
|
||||
primary_key: j(item.primaryKey), old_data: j(item.oldData), new_data: j(item.newData) };
|
||||
case "delete":
|
||||
return { id: item.id, type: "delete", schema, table, primary_key: j(item.primaryKey) };
|
||||
case "alter_table":
|
||||
return { id: item.id, type: "alter_table", schema, table, sql: item.sql, rollback_sql: "" };
|
||||
case "bulk_insert":
|
||||
return { id: item.id, type: "bulk_insert", schema, table,
|
||||
columns: item.columns ?? [], rows: item.rows ?? [] };
|
||||
case "drop_table":
|
||||
return { id: item.id, type: "drop_table", schema, table };
|
||||
case "empty_table":
|
||||
return { id: item.id, type: "empty_table", schema, table };
|
||||
default:
|
||||
return { id: item.id, type: item.type, sql: item.sql };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user