v0.7.7: PG roles/grants, table maintenance, Create/Edit Table + atomic column-reorder rebuild, FK management, table options (#13)
* feat(models): Change::RebuildTable + role/privilege/readiness/maintenance structs (Task 1.1) * feat(introspection): role/privilege/tablespace/rebuild-readiness query builders (Task 1.2) * feat(lib): rebuild_table payload, table/role object kinds, v0.7.7 capabilities + types (Task 1.3) * feat(object_crud): table_ddl builder (create/edit-diff/options/drop) + build_ddl table arm (Task 2.1) * feat(object_crud): role_ddl builder (create/edit/drop/grant/revoke) + build_ddl role arm (Task 2.2) * feat(object_crud): rebuild_script assembler + fk/grant/sequence introspection queries (Task 2.3) * feat(commands): transactional RebuildTable arm + build_rebuild_script + get_table_columns commands (Task 2.4) * feat(commands): get_roles/privileges/readiness/tablespaces + run_maintenance (Task 2.5) * feat(commands): role/privilege/rebuild/maintenance typed wrappers (Task 3.1) * feat(store): roles slice + openFormTab table/role dedup (Task 3.2) * feat(ui): TableForm (create/edit-diff/rebuild) + cross-schema FK with ON DELETE/UPDATE (Task 4.1) * feat(ui): RoleForm + RoleDetail + RoleGrantsEditor (Task 4.2) * feat(ui): wire roles into object registry + explorer (Task 4.3) * feat(ui): Edit Table + maintenance menu + Create Table toolbar + rebuild badge (Task 4.4) * chore: bump version to 0.7.7 (Task 5.1) * docs: sync README/ROADMAP/AGENTS to v0.7.7 (Task 5.2) * test: v0.7.7 manual smoke checklist (Task 5.3) * chore: sync Cargo.lock to v0.7.7 * style(ui): match TableForm visual layout to shared object-form rows (remove stray padding) * feat(ui): table creator grid redesign + per-column FK slide-in panel with relationship controls * style(ui): table-creator columns grid — bordered cells like the data table view * feat(ui): table creator — type dropdown, PK/Nullable rules, drag-to-reorder columns (vertical), tidy add-column row * feat(ui): table creator — single-PK exclusivity, name on top, fixed column widths + contained h-scroll, no-wrap constraints * style(ui): table creator — constraints column sized to content (no clip), overscroll containment * style(ui): table creator — overscrollBehavior none on both scroll containers, add-column row py-2 * feat(ui): table creator — centered add-column button, schema dropdown auto-selected to current schema * style(ui): table creator — columns grid fills container width, scrolls only when too narrow * feat(ui): table creator — shorthand PG types in dropdown, Auto-Increment only for int-family * feat(ui): FK panel — animate in/out from the right, flush create-table-style rows * fix(ui): FK panel — smooth slide (willChange + eased), local column options from the form grid * feat(ui): FK composer (multi-column, type preview, gated sections) + composite PK + constraints cog in actions col + auto-named FKs * fix(ui): FK panel blocks unnamed table with hint; Foreign keys section lists staged + DB FKs * style(ui): FK panel — transparent full-width selects, title↔select borders, 2-col column picker * style(ui): FK panel — vertical divider between local/ref columns * style(ui): FK panel — section title padding py-2 * style(ui): FK panel — remove row margins + select/text padding in column picker * style(ui): FK panel — flush column picker (no section padding), border-r + py-1 cells * style(ui): FK panel — cell padding + row borders on column picker (user polish) * feat: FK inline into CREATE TABLE (single staged change in create mode); touch up class * style(ui): hide FK icon on PK columns; fix grid header/row column alignment * feat(ui): SQL preview in table creator uses read-only Monaco editor * feat(ui): Foreign keys section — relation rows with icon + Edit/Remove actions * style(ui): FK relation row — label, icon, table on one line * style(ui): FormRow selects transparent like FK panel; test matches FK relation label * style(ui): form-row focus outline amber-400/20 (subtle) * style(ui): SQL view — remove px-4 py-3 wrapper padding * feat(ui): Create Table button label; auto tree refresh after schema-modifying commits; table name on staged ddl card * fix(roles): cast rolconnlimit::int8 to fix panic deserializing int4 into i64 * fix(privileges): cast privilege_type::text to avoid domain-array panic; aclexplode for sequences/schemas (role_sequence_grants removed in PG15, schema_privileges never existed) * feat(ui): role privileges sections collapsible (collapsed by default) with 5-entry preview + fade + Show more * feat(ui): role privileges — collapsed state shows first 5 with fade; expand reveals all * chore(test): drop unused fireEvent import in RoleDetail test * style(ui): hide chevron when section has 5 or fewer entries * docs: README/ROADMAP/AGENTS — v0.7.7 shipped details (roles/grants, table editor, FK composer, privilege explorer); ROADMAP adds v0.7.8 Next up (quick wins) * docs(readme): collapse Recent changes and Key features into details blocks * docs(readme): move hero caption under image; align download + comparison table columns * docs(readme): collapse only body content — keep Recent Changes / Key Features headings visible * docs(readme): per-subsection collapsibles with meaningful summary labels
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import tauriConf from "../../src-tauri/tauri.conf.json";
|
||||
|
||||
describe("tauri bundle config (v0.7.6)", () => {
|
||||
describe("tauri bundle config (v0.7.7)", () => {
|
||||
it("declares bundled pg_tools resources", () => {
|
||||
expect(tauriConf.bundle.resources).toContain("resources/pg_tools/*");
|
||||
});
|
||||
it("version is 0.7.6", () => {
|
||||
expect(tauriConf.version).toBe("0.7.6");
|
||||
it("version is 0.7.7", () => {
|
||||
expect(tauriConf.version).toBe("0.7.7");
|
||||
});
|
||||
});
|
||||
@@ -63,6 +63,23 @@ describe("buildChangeSql", () => {
|
||||
});
|
||||
});
|
||||
|
||||
function rebuildItem(sql: string): QueueItem {
|
||||
return {
|
||||
id: "ch-rb1", type: "rebuild_table", sql, status: "pending",
|
||||
createdAt: 0,
|
||||
} as unknown as QueueItem;
|
||||
}
|
||||
|
||||
describe("rebuild_table payload", () => {
|
||||
it("builds a rebuild_table payload with the script", () => {
|
||||
const p = buildChangePayload(rebuildItem("CREATE TABLE _t();"));
|
||||
expect(p).toEqual({ id: "ch-rb1", type: "rebuild_table", sql: "CREATE TABLE _t();" });
|
||||
});
|
||||
it("buildChangeSql returns the script verbatim", () => {
|
||||
expect(buildChangeSql(rebuildItem("SELECT 1;"))).toBe("SELECT 1;");
|
||||
});
|
||||
});
|
||||
|
||||
const ddlItem: QueueItem = {
|
||||
id: "ch-1", type: "ddl",
|
||||
sql: "CREATE TYPE public.role AS ENUM ('admin')",
|
||||
|
||||
@@ -59,6 +59,8 @@ export function buildChangeSql(item: QueueItem): string {
|
||||
return `DELETE FROM ${t}`;
|
||||
case "ddl":
|
||||
return item.sql ?? "";
|
||||
case "rebuild_table":
|
||||
return item.sql ?? "";
|
||||
case "drop_table":
|
||||
return `DROP TABLE ${t}`;
|
||||
default:
|
||||
@@ -88,6 +90,8 @@ export function buildChangePayload(item: QueueItem): ChangePayload {
|
||||
return { id: item.id, type: "empty_table", schema, table };
|
||||
case "ddl":
|
||||
return { id: item.id, type: "ddl", sql: item.sql };
|
||||
case "rebuild_table":
|
||||
return { id: item.id, type: "rebuild_table", sql: item.sql };
|
||||
default:
|
||||
return { id: item.id, type: item.type, sql: item.sql };
|
||||
}
|
||||
|
||||
@@ -7,6 +7,13 @@ vi.mock("@tauri-apps/api/core", () => ({
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import {
|
||||
testConnection,
|
||||
getRoles,
|
||||
getRolePrivileges,
|
||||
getTableColumns,
|
||||
getTableRebuildReadiness,
|
||||
runMaintenance,
|
||||
getTablespaces,
|
||||
buildRebuildScript,
|
||||
dbConnect,
|
||||
dbDisconnect,
|
||||
getDatabases,
|
||||
@@ -332,4 +339,88 @@ describe("object management commands (v0.7.5)", () => {
|
||||
await getObjectDependencies("c1", "public", "table", "orders");
|
||||
expect(invoke).toHaveBeenCalledWith("get_object_dependencies", { connectionId: "c1", schema: "public", objectType: "table", name: "orders" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("v0.7.7 command wrappers", () => {
|
||||
it("getRoles calls get_roles", async () => {
|
||||
const mockRoles = [
|
||||
{
|
||||
name: "postgres",
|
||||
superuser: true,
|
||||
inherit: true,
|
||||
create_db: true,
|
||||
create_role: true,
|
||||
can_login: true,
|
||||
replication: true,
|
||||
bypass_rls: false,
|
||||
connection_limit: -1,
|
||||
valid_until: null,
|
||||
memberships: [],
|
||||
},
|
||||
];
|
||||
vi.mocked(invoke).mockResolvedValueOnce(mockRoles);
|
||||
|
||||
const result = await getRoles("c1");
|
||||
|
||||
expect(invoke).toHaveBeenCalledWith("get_roles", { connectionId: "c1" });
|
||||
expect(result).toEqual(mockRoles);
|
||||
});
|
||||
|
||||
it("getRolePrivileges calls get_role_privileges with role", async () => {
|
||||
vi.mocked(invoke).mockResolvedValueOnce([]);
|
||||
|
||||
const result = await getRolePrivileges("c1", "app");
|
||||
|
||||
expect(invoke).toHaveBeenCalledWith("get_role_privileges", { connectionId: "c1", role: "app" });
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("getTableColumns calls get_table_columns", async () => {
|
||||
vi.mocked(invoke).mockResolvedValueOnce([]);
|
||||
|
||||
const result = await getTableColumns("c1", "public", "users");
|
||||
|
||||
expect(invoke).toHaveBeenCalledWith("get_table_columns", { connectionId: "c1", schema: "public", table: "users" });
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("getTableRebuildReadiness calls get_table_rebuild_readiness", async () => {
|
||||
const mockReadiness = { ok: true, reasons: [] };
|
||||
vi.mocked(invoke).mockResolvedValueOnce(mockReadiness);
|
||||
|
||||
const result = await getTableRebuildReadiness("c1", "public", "users");
|
||||
|
||||
expect(invoke).toHaveBeenCalledWith("get_table_rebuild_readiness", { connectionId: "c1", schema: "public", table: "users" });
|
||||
expect(result).toEqual(mockReadiness);
|
||||
});
|
||||
|
||||
it("runMaintenance calls run_maintenance with action", async () => {
|
||||
const mockResult = { duration_ms: 5, message: "ok" };
|
||||
vi.mocked(invoke).mockResolvedValueOnce(mockResult);
|
||||
|
||||
const result = await runMaintenance("c1", "public", "users", "vacuum");
|
||||
|
||||
expect(invoke).toHaveBeenCalledWith("run_maintenance", { connectionId: "c1", schema: "public", table: "users", action: "vacuum" });
|
||||
expect(result).toEqual(mockResult);
|
||||
});
|
||||
|
||||
it("getTablespaces calls get_tablespaces", async () => {
|
||||
vi.mocked(invoke).mockResolvedValueOnce([]);
|
||||
|
||||
const result = await getTablespaces("c1");
|
||||
|
||||
expect(invoke).toHaveBeenCalledWith("get_tablespaces", { connectionId: "c1" });
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("buildRebuildScript calls build_rebuild_script with newColumns", async () => {
|
||||
const mockScript = "CREATE TABLE _t();";
|
||||
const cols = [{ name: "id", type: "int", nullable: false, default: null, is_pk: true }];
|
||||
vi.mocked(invoke).mockResolvedValueOnce(mockScript);
|
||||
|
||||
const result = await buildRebuildScript("c1", "public", "users", cols);
|
||||
|
||||
expect(invoke).toHaveBeenCalledWith("build_rebuild_script", { connectionId: "c1", schema: "public", table: "users", newColumns: cols });
|
||||
expect(result).toEqual(mockScript);
|
||||
});
|
||||
});
|
||||
+33
-1
@@ -1,7 +1,15 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import type { Connection, ConnectionInput, ConnectionTestResult, Folder, FolderInput, Tag, TagInput, Settings, ImportResult, TableInfo, QueryResult, BackupOptions, RestoreOptions, SyncOptions, PgToolStatus, FunctionInfo, TriggerInfo, SequenceInfo, EnumInfo, ExtensionInfo, SchemaGraph, IndexInfo, ConstraintInfo, RecentConnection, ObjectSearchHit, DependencyInfo } from "./types";
|
||||
import type { Connection, ConnectionInput, ConnectionTestResult, Folder, FolderInput, Tag, TagInput, Settings, ImportResult, TableInfo, QueryResult, BackupOptions, RestoreOptions, SyncOptions, PgToolStatus, FunctionInfo, TriggerInfo, SequenceInfo, EnumInfo, ExtensionInfo, SchemaGraph, IndexInfo, ConstraintInfo, RecentConnection, ObjectSearchHit, DependencyInfo, RoleInfo, PrivilegeEntry, RebuildReadiness, MaintenanceResult, TablespaceInfo, ColumnInfo } from "./types";
|
||||
import type { FilterRule, SortRule } from "../stores/dbViewerStore";
|
||||
import type { ChangePayload } from "./changePayload";
|
||||
import { buildObjectDdl as buildObjectDdlImpl, type ObjectKind, type DdlParams } from "./objectCrud";
|
||||
|
||||
export { type ObjectKind, type DdlParams };
|
||||
|
||||
/** Build one or more SQL statements for an object CRUD operation. */
|
||||
export function buildObjectDdl(connectionId: string, kind: ObjectKind, params: DdlParams): Promise<string[]> {
|
||||
return buildObjectDdlImpl(connectionId, kind, params);
|
||||
}
|
||||
|
||||
// NOTE on argument key naming:
|
||||
// Tauri v2's #[tauri::command] macro converts Rust snake_case parameter names
|
||||
@@ -319,4 +327,28 @@ export async function getObjectDdl(connectionId: string, schema: string, objectT
|
||||
}
|
||||
export async function getObjectDependencies(connectionId: string, schema: string, objectType: string, name: string): Promise<DependencyInfo[]> {
|
||||
return invoke<DependencyInfo[]>("get_object_dependencies", { connectionId, schema, objectType, name });
|
||||
}
|
||||
|
||||
// ─── v0.7.7: Roles / privileges / rebuild / maintenance ──────────
|
||||
|
||||
export async function getRoles(connectionId: string): Promise<RoleInfo[]> {
|
||||
return invoke<RoleInfo[]>("get_roles", { connectionId });
|
||||
}
|
||||
export async function getRolePrivileges(connectionId: string, role: string): Promise<PrivilegeEntry[]> {
|
||||
return invoke<PrivilegeEntry[]>("get_role_privileges", { connectionId, role });
|
||||
}
|
||||
export async function getTableColumns(connectionId: string, schema: string, table: string): Promise<ColumnInfo[]> {
|
||||
return invoke<ColumnInfo[]>("get_table_columns", { connectionId, schema, table });
|
||||
}
|
||||
export async function getTableRebuildReadiness(connectionId: string, schema: string, table: string): Promise<RebuildReadiness> {
|
||||
return invoke<RebuildReadiness>("get_table_rebuild_readiness", { connectionId, schema, table });
|
||||
}
|
||||
export async function runMaintenance(connectionId: string, schema: string, table: string, action: "vacuum" | "analyze" | "reindex"): Promise<MaintenanceResult> {
|
||||
return invoke<MaintenanceResult>("run_maintenance", { connectionId, schema, table, action });
|
||||
}
|
||||
export async function getTablespaces(connectionId: string): Promise<TablespaceInfo[]> {
|
||||
return invoke<TablespaceInfo[]>("get_tablespaces", { connectionId });
|
||||
}
|
||||
export async function buildRebuildScript<C extends { name: string; type: string; nullable: boolean; default: string | null; is_pk: boolean }[]>(connectionId: string, schema: string, table: string, newColumns: C): Promise<string> {
|
||||
return invoke<string>("build_rebuild_script", { connectionId, schema, table, newColumns });
|
||||
}
|
||||
@@ -7,6 +7,7 @@ describe("dbCapabilities", () => {
|
||||
expect(c).toEqual<DbCapabilities>({
|
||||
explorer: true, queries: true, objects: true, visualizer: true,
|
||||
tools: true, editing: true, import: true, ddl: true, objectCrud: true,
|
||||
maintenance: true, roles: true, tableManagement: true,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -38,6 +39,7 @@ describe("dbCapabilities", () => {
|
||||
expect(DB_CAPABILITIES.redis).toEqual<DbCapabilities>({
|
||||
explorer: false, queries: false, objects: false, visualizer: false,
|
||||
tools: false, editing: false, import: false, ddl: false, objectCrud: false,
|
||||
maintenance: false, roles: false, tableManagement: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -69,4 +71,22 @@ describe("objectCrud capability", () => {
|
||||
it("unknown types get objectCrud false", () => {
|
||||
expect(getCapabilities("oracle").objectCrud).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("v0.7.7 capabilities", () => {
|
||||
it("postgresql has maintenance, roles, tableManagement", () => {
|
||||
expect(DB_CAPABILITIES.postgresql.maintenance).toBe(true);
|
||||
expect(DB_CAPABILITIES.postgresql.roles).toBe(true);
|
||||
expect(DB_CAPABILITIES.postgresql.tableManagement).toBe(true);
|
||||
});
|
||||
it("mysql/sqlite/redis disable maintenance, roles, tableManagement", () => {
|
||||
for (const t of ["mysql", "sqlite", "redis"] as const) {
|
||||
expect(DB_CAPABILITIES[t].maintenance).toBe(false);
|
||||
expect(DB_CAPABILITIES[t].roles).toBe(false);
|
||||
expect(DB_CAPABILITIES[t].tableManagement).toBe(false);
|
||||
}
|
||||
});
|
||||
it("getCapabilities is safe for unknown types", () => {
|
||||
expect(getCapabilities("bogus").maintenance).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -19,15 +19,22 @@ export interface DbCapabilities {
|
||||
ddl: boolean;
|
||||
/** Create / edit / drop PostgreSQL objects via the changes queue. */
|
||||
objectCrud: boolean;
|
||||
/** Table maintenance (VACUUM/ANALYZE/REINDEX) + rebuild-table. */
|
||||
maintenance: boolean;
|
||||
/** Role browsing/management. */
|
||||
roles: boolean;
|
||||
/** Rebuild-table (rewrite in place) support. */
|
||||
tableManagement: boolean;
|
||||
}
|
||||
|
||||
const ALL_FALSE: DbCapabilities = {
|
||||
explorer: false, queries: false, objects: false, visualizer: false,
|
||||
tools: false, editing: false, import: false, ddl: false, objectCrud: false,
|
||||
maintenance: false, roles: false, tableManagement: false,
|
||||
};
|
||||
|
||||
export const DB_CAPABILITIES: Record<DbType, DbCapabilities> = {
|
||||
postgresql: { ...ALL_FALSE, explorer: true, queries: true, objects: true, visualizer: true, tools: true, editing: true, import: true, ddl: true, objectCrud: true },
|
||||
postgresql: { ...ALL_FALSE, explorer: true, queries: true, objects: true, visualizer: true, tools: true, editing: true, import: true, ddl: true, objectCrud: true, maintenance: true, roles: true, tableManagement: true },
|
||||
mysql: { ...ALL_FALSE, explorer: true, queries: true, editing: true, import: true, ddl: true },
|
||||
sqlite: { ...ALL_FALSE, explorer: true, queries: true, visualizer: true, editing: true, import: true, ddl: true },
|
||||
redis: { ...ALL_FALSE },
|
||||
|
||||
@@ -4,7 +4,7 @@ import { describe, it, expect } from "vitest";
|
||||
import agents from "../../AGENTS.md?raw";
|
||||
import readme from "../../README.md?raw";
|
||||
|
||||
describe("v0.7.6 docs coverage", () => {
|
||||
describe("v0.7.7 docs coverage", () => {
|
||||
it("AGENTS.md marks inline cell editing complete", () => {
|
||||
expect(agents).toContain("Inline cell editing");
|
||||
expect(agents).toMatch(/Inline cell editing \| ✅/);
|
||||
@@ -24,8 +24,8 @@ describe("v0.7.6 docs coverage", () => {
|
||||
expect(agents).toMatch(/Connection status indicator on cards \| ✅/);
|
||||
expect(agents).toMatch(/Move-to-folder bulk action \| ✅/);
|
||||
});
|
||||
it("README declares v0.7.6", () => {
|
||||
expect(readme).toContain("0.7.6");
|
||||
it("README declares v0.7.7", () => {
|
||||
expect(readme).toContain("0.7.7");
|
||||
});
|
||||
it("AGENTS.md marks schema CRUD complete", () => {
|
||||
expect(agents).toMatch(/Schema CRUD \| ✅/);
|
||||
@@ -59,9 +59,9 @@ describe("v0.7.6 docs coverage", () => {
|
||||
it("AGENTS.md marks the Objects view tabbed workspace complete", () => {
|
||||
expect(agents).toMatch(/Objects view tabbed workspace \| ✅/);
|
||||
});
|
||||
it("README links to v0.7.6 assets in both download tables", () => {
|
||||
expect(readme).toContain("releases/download/v0.7.6/");
|
||||
expect(readme).toContain("Gridline_0.7.6_aarch64.dmg");
|
||||
expect(readme).toContain("Gridline-0.7.6-1.x86_64.rpm");
|
||||
it("README links to v0.7.7 assets in both download tables", () => {
|
||||
expect(readme).toContain("releases/download/v0.7.7/");
|
||||
expect(readme).toContain("Gridline_0.7.7_aarch64.dmg");
|
||||
expect(readme).toContain("Gridline-0.7.7-1.x86_64.rpm");
|
||||
});
|
||||
});
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
getAvailableExtensions,
|
||||
initialCrudParams,
|
||||
} from "./objectCrud";
|
||||
import type { CrudItem } from "./objectCrud";
|
||||
|
||||
describe("buildObjectDdl", () => {
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
@@ -460,4 +461,31 @@ describe("dropCrudParams", () => {
|
||||
action: { op: "drop", arg_types: ["int"] },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("table/role crud params", () => {
|
||||
it("initialCrudParams(table, create) starts empty with columns", () => {
|
||||
const p = initialCrudParams("table", { schema: "public", name: "" }, "create");
|
||||
expect(p).toEqual({ schema: "public", name: "", action: { op: "create", columns: [] } });
|
||||
});
|
||||
it("initialCrudParams(table, edit) prefills columns", () => {
|
||||
const item: CrudItem = { schema: "public", name: "users", columns: ["id"], columnMeta: [
|
||||
{ name: "id", type: "integer", nullable: false, is_pk: true, default: null },
|
||||
] };
|
||||
const p = initialCrudParams("table", item, "edit");
|
||||
expect((p as any).action.op).toBe("edit");
|
||||
expect((p as any).action.old_columns).toEqual(item.columnMeta);
|
||||
});
|
||||
it("initialCrudParams(role, create) starts empty with options", () => {
|
||||
const p = initialCrudParams("role", { schema: "", name: "" }, "create");
|
||||
expect(p).toEqual({ schema: "", name: "", action: { op: "create", login: false, superuser: false, createdb: false, createrole: false, inherit: true, replication: false, bypassrls: false, connection_limit: -1, valid_until: "", password: "", members: [] } });
|
||||
});
|
||||
it("dropCrudParams(role) drops by name", () => {
|
||||
const p = dropCrudParams("role", { schema: "", name: "app" });
|
||||
expect(p).toEqual({ schema: "", name: "app", action: { op: "drop" } });
|
||||
});
|
||||
it("dropCrudParams(table) drops the table", () => {
|
||||
const p = dropCrudParams("table", { schema: "public", name: "users" });
|
||||
expect(p).toEqual({ schema: "public", name: "users", action: { op: "drop" } });
|
||||
});
|
||||
});
|
||||
+52
-1
@@ -14,7 +14,9 @@ export type ObjectKind =
|
||||
| "constraint"
|
||||
| "function"
|
||||
| "procedure"
|
||||
| "trigger";
|
||||
| "trigger"
|
||||
| "table"
|
||||
| "role";
|
||||
|
||||
/// Opaque payload for a build: `{ schema, name, action: { op, ... } }`.
|
||||
/// The concrete shape is validated server-side by each kind's params struct.
|
||||
@@ -60,6 +62,18 @@ export interface CrudItem {
|
||||
columns?: string[];
|
||||
// constraint (ConstraintInfo)
|
||||
contype?: "CHECK" | "UNIQUE" | "EXCLUSION";
|
||||
// table (columnMeta carries the snapshot for edit diffs)
|
||||
columnMeta?: { name: string; type: string; nullable: boolean; is_pk: boolean; default: string | null }[];
|
||||
// role (RoleInfo shape used for prefill)
|
||||
superuser?: boolean;
|
||||
inherit?: boolean;
|
||||
create_db?: boolean;
|
||||
create_role?: boolean;
|
||||
can_login?: boolean;
|
||||
replication?: boolean;
|
||||
bypass_rls?: boolean;
|
||||
connection_limit?: number;
|
||||
valid_until?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -194,6 +208,40 @@ export function initialCrudParams(
|
||||
when: null,
|
||||
},
|
||||
};
|
||||
case "table":
|
||||
return {
|
||||
schema,
|
||||
name: mode === "edit" ? item.name : "",
|
||||
action: {
|
||||
op: mode === "edit" ? "edit" : "create",
|
||||
columns: mode === "edit"
|
||||
? (item.columnMeta ?? []).map((c) => ({
|
||||
name: c.name, type: c.type, nullable: c.nullable,
|
||||
default: c.default ?? null, is_pk: c.is_pk,
|
||||
}))
|
||||
: [],
|
||||
old_columns: mode === "edit" ? (item.columnMeta ?? []) : undefined,
|
||||
},
|
||||
};
|
||||
case "role":
|
||||
return {
|
||||
schema,
|
||||
name: mode === "edit" ? item.name : "",
|
||||
action: {
|
||||
op: "create",
|
||||
login: mode === "edit" ? (item.can_login ?? false) : false,
|
||||
superuser: mode === "edit" ? (item.superuser ?? false) : false,
|
||||
createdb: mode === "edit" ? (item.create_db ?? false) : false,
|
||||
createrole: mode === "edit" ? (item.create_role ?? false) : false,
|
||||
inherit: mode === "edit" ? (item.inherit ?? true) : true,
|
||||
replication: mode === "edit" ? (item.replication ?? false) : false,
|
||||
bypassrls: mode === "edit" ? (item.bypass_rls ?? false) : false,
|
||||
connection_limit: mode === "edit" ? (item.connection_limit ?? -1) : -1,
|
||||
valid_until: mode === "edit" ? (item.valid_until ?? "") : "",
|
||||
password: "",
|
||||
members: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,6 +251,9 @@ export function initialCrudParams(
|
||||
*/
|
||||
export function dropCrudParams(kind: ObjectKind, item: CrudItem): DdlParams {
|
||||
const base = { schema: item.schema, name: item.name };
|
||||
if (kind === "table" || kind === "role") {
|
||||
return { ...base, action: { op: "drop" } };
|
||||
}
|
||||
if (kind === "trigger") {
|
||||
return {
|
||||
...base,
|
||||
|
||||
@@ -19,6 +19,11 @@ import type {
|
||||
IndexInfo,
|
||||
ConstraintInfo,
|
||||
RecentConnection,
|
||||
RoleInfo,
|
||||
PrivilegeEntry,
|
||||
RebuildReadiness,
|
||||
MaintenanceResult,
|
||||
ObjectType,
|
||||
} from "./types";
|
||||
|
||||
describe("ActiveView", () => {
|
||||
@@ -495,4 +500,31 @@ describe("v0.5.0 types", () => {
|
||||
const c = { favorite: true } as Connection;
|
||||
expectTypeOf(c.favorite).toEqualTypeOf<boolean>();
|
||||
});
|
||||
});
|
||||
|
||||
describe("v0.7.7 types", () => {
|
||||
it("ObjectType includes roles", () => {
|
||||
const t: ObjectType = "roles";
|
||||
expect(t).toBe("roles");
|
||||
});
|
||||
it("ChangeItemType includes rebuild_table", () => {
|
||||
const t: ChangeItemType = "rebuild_table";
|
||||
expect(t).toBe("rebuild_table");
|
||||
});
|
||||
it("RoleInfo shape", () => {
|
||||
const r: RoleInfo = {
|
||||
name: "app", superuser: false, inherit: true, create_db: false, create_role: false,
|
||||
can_login: true, replication: false, bypass_rls: false, connection_limit: -1,
|
||||
valid_until: null, memberships: [],
|
||||
};
|
||||
expect(r.name).toBe("app");
|
||||
});
|
||||
it("PrivilegeEntry/RebuildReadiness/MaintenanceResult shapes", () => {
|
||||
const pe: PrivilegeEntry = { object_class: "table", schema: "public", name: "users", privileges: ["SELECT"], grantable: false };
|
||||
const rr: RebuildReadiness = { ok: true, reasons: [] };
|
||||
const mr: MaintenanceResult = { duration_ms: 1, message: "ok" };
|
||||
expect(pe.object_class).toBe("table");
|
||||
expect(rr.ok).toBe(true);
|
||||
expect(mr.duration_ms).toBe(1);
|
||||
});
|
||||
});
|
||||
+48
-1
@@ -202,6 +202,7 @@ export type ChangeItemType =
|
||||
| "drop_index"
|
||||
| "bulk_insert"
|
||||
| "empty_table"
|
||||
| "rebuild_table"
|
||||
| "ddl";
|
||||
|
||||
export interface ChangeItem {
|
||||
@@ -324,7 +325,8 @@ export type ObjectType =
|
||||
| "extensions"
|
||||
| "indexes"
|
||||
| "constraints"
|
||||
| "procedures";
|
||||
| "procedures"
|
||||
| "roles";
|
||||
|
||||
export interface ObjectSearchHit {
|
||||
name: string;
|
||||
@@ -352,6 +354,51 @@ export interface BackupJob {
|
||||
completed_at: string | null;
|
||||
}
|
||||
|
||||
// ─── Roles / Privileges / Maintenance (v0.7.7) ──────────────────
|
||||
|
||||
export interface RoleMembership {
|
||||
role: string;
|
||||
member: string;
|
||||
grantor: string;
|
||||
admin_option: boolean;
|
||||
}
|
||||
|
||||
export interface RoleInfo {
|
||||
name: string;
|
||||
superuser: boolean;
|
||||
inherit: boolean;
|
||||
create_db: boolean;
|
||||
create_role: boolean;
|
||||
can_login: boolean;
|
||||
replication: boolean;
|
||||
bypass_rls: boolean;
|
||||
connection_limit: number;
|
||||
valid_until: string | null;
|
||||
memberships: RoleMembership[];
|
||||
}
|
||||
|
||||
export interface PrivilegeEntry {
|
||||
object_class: "table" | "sequence" | "routine" | "schema" | "database";
|
||||
schema: string | null;
|
||||
name: string;
|
||||
privileges: string[];
|
||||
grantable: boolean;
|
||||
}
|
||||
|
||||
export interface RebuildReadiness {
|
||||
ok: boolean;
|
||||
reasons: string[];
|
||||
}
|
||||
|
||||
export interface MaintenanceResult {
|
||||
duration_ms: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface TablespaceInfo {
|
||||
name: string;
|
||||
}
|
||||
|
||||
// ─── Schema Visualizer Types ────────────────────────────────────
|
||||
|
||||
export interface GraphColumn {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, it, expect } from "vitest";
|
||||
import pkg from "../../package.json";
|
||||
|
||||
describe("version", () => {
|
||||
it("declares v0.7.6 across the app shell", () => {
|
||||
expect(pkg.version).toBe("0.7.6");
|
||||
it("declares v0.7.7 across the app shell", () => {
|
||||
expect(pkg.version).toBe("0.7.7");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user