Files
gridline/src/lib/types.test.ts
T
adrianbonpin 16888460b7 v0.5.0 — Grid Interactivity, Home Polish, Deeper PostgreSQL (#8)
* chore: bump version to 0.5.0 (Task 1)

* feat(store): v7 migration — favorites + recent_connections (Task 2)

* feat(models): add favorite to Connection + Store CRUD (Task 3)

* feat(types): ColumnInfo editability + IndexInfo/ConstraintInfo/RecentConnection (Task 4)

* chore: bump version to 0.5.0 (Task 1) — lockfile

* feat(commands): typed wrappers for favorites/recents/indexes/constraints (Task 5)

* feat(db): PG indexes/constraints queries + matview UNION in tables (Task 6)

* feat(db): get_table_data editability flags + ctid/rowid locator (Task 7)

* feat(db): execute_change no-PK locator guard + affected-count check (Task 8)

* feat(db): preserve bigint precision as string on PG read path (Task 9)

* feat(db): get_indexes / get_constraints commands (Task 10)

* feat(store): favorites + recents Store methods (Task 11)

* feat(commands): favorites/recents IPC + register indexes/constraints (Task 12)

* feat(store): connectionStore favorites/recents/move-selection (Task 13)

* feat(lib): recent-connections pure helpers (Task 14)

* feat(store): dbViewerStore indexes/constraints + stageCellEdit (Task 15)

* feat(grid): pure editability + filter-operator + cell transform (Task 16)

* feat(grid): pure keyboard-nav helper (Task 17)

* feat(grid): CellEditor inline editor (Task 18)

* feat(grid): CellContextMenu + RowDetailDrawer (Task 19)

* feat(grid): focus model + keyboard nav + inline edit + copy + context menu (Task 20)

* feat(db-viewer): FilterBuilder drag-and-drop + type-aware operators (Task 21)

* feat(db-viewer): ObjectExplorer indexes/constraints/procedures + matview icon (Task 22)

* feat(home): ConnectionCard favorite star + on-demand StatusDot (Task 23)

* feat(home): move-to-folder + recents strip + status wiring (Task 24)

* feat(db-viewer): grid wiring + matview read-only + post-commit refetch (Task 25)

* docs: v0.5.0 status + roadmap updates (Task 26)

* polish: empty/error/loading states for v0.5.0 surfaces (Task 28)

* feat(home): duplicateConnection + useConnectionStatus hook, drop StatusDot (FEAT-A)

* feat(home): connection card kebab menu — favorite/test/manage (FEAT-B)

* fix(home): populate server_version/latency_ms in test_connection + clean online display

* style(home): swap grab handle and kebab positions on connection card

* style(home): nudge kebab menu to right-1

* style(home): nudge kebab menu to right-0.5

* feat(home): Escape clears + exits focused search

* feat(grid): context-menu View/Select Row, outside-click close, Esc cancels edit, FK reference (GRID-A)

* feat(grid): smart CellEditor — enum select, FK searchable dropdown, textarea height (GRID-B)

* feat(grid): enums + FK options fed into CellEditor (GRID-C)

* feat(grid): FK dropdown display-column labels + placeholder + empty state

* fix(grid): FK dropdown renders as fixed overlay to avoid clipping

* fix(grid): portal FK dropdown to body + FK reference icon instead of click-to-open

* style(grid): move FK reference icon to the start of the cell

* feat(grid): optimistic staged cell values + pending dot, cleared on refetch

* fix(grid): queue is source of truth for staged values — value diff, Clear All clears dots, same-cell edits replace

* fix(db-viewer): type getLocator for staged-value matching

* test(db-viewer): unit-test deriveStagedValues; fix activeTab null guard

* fix(db-viewer): pass table prop to VirtualDataGrid — staged edits now carry the table name

* test(db-viewer): use index access instead of .at() for TS lib target

* feat(grid): FK dropdown options as one-row column cells (FK-reference style, cap 5)

* style(grid): FK dropdown — values only, fixed 360px width, FK-viewer surface styling

* style(grid): harden FK dropdown minWidth to 360px

* style(grid): cap FK dropdown cells at 3

* style(grid): cap FK dropdown cells at 4

* fix(grid): pending dot clears on commit — values stay until refetch

* fix(db): deserialize pg_attribute char columns as i8 — no more panic on get_table_data

* docs: reflect grid interactivity, smart editors, optimistic queue, FK reference, kebab status
2026-08-03 02:42:24 +08:00

498 lines
14 KiB
TypeScript

import { describe, it, expect, expectTypeOf } from "vitest";
import type {
Connection,
ConnectionInput,
ActiveView,
TableInfo,
ColumnInfo,
QueryResult,
ChangeItem,
ChangeItemType,
ChangeStatus,
DbViewerTab,
ConnectionTestResult,
SchemaGraph,
TableNode,
GraphColumn,
Relationship,
Settings,
IndexInfo,
ConstraintInfo,
RecentConnection,
} from "./types";
describe("ActiveView", () => {
it("includes db-viewer", () => {
const view: ActiveView = "db-viewer";
expect(view).toBe("db-viewer");
});
});
describe("Connection", () => {
it("accepts new SSH/SSL fields", () => {
const conn: Connection = {
id: "c1",
name: "Test",
db_type: "postgresql",
host: "localhost",
port: 5432,
username: "admin",
folder_id: null,
keychain_ref: null,
tag_ids: [],
favorite: false,
created_at: "2024-01-01T00:00:00Z",
updated_at: "2024-01-01T00:00:00Z",
// new SSH/SSL fields
database: "mydb",
ssh_host: "bastion.example.com",
ssh_port: 22,
ssh_user: "tunnel",
ssh_auth_method: "key",
ssh_private_key_path: "/home/user/.ssh/id_rsa",
ssl_mode: "require",
ssl_ca_path: "/etc/ssl/certs/ca.pem",
ssl_cert_path: "/etc/ssl/certs/client.crt",
ssl_key_path: "/etc/ssl/certs/client.key",
};
expect(conn.ssh_host).toBe("bastion.example.com");
expect(conn.database).toBe("mydb");
expect(conn.ssl_mode).toBe("require");
});
it("does not include sensitive SSH/SSL fields (password/ssh_passphrase)", () => {
// TypeScript compile check: these should not be assignable
const conn: Connection = {
id: "c2",
name: "Minimal",
db_type: "postgresql",
host: "localhost",
port: null,
username: null,
folder_id: null,
keychain_ref: null,
tag_ids: [],
favorite: false,
created_at: "2024-01-01T00:00:00Z",
updated_at: "2024-01-01T00:00:00Z",
};
// @ts-expect-error - password must NOT exist on Connection
conn.password;
// @ts-expect-error - ssh_passphrase must NOT exist on Connection
conn.ssh_passphrase;
expect(conn.host).toBe("localhost");
});
it("accepts SSH fields as optional (minimal connection)", () => {
const conn: Connection = {
id: "c3",
name: "Minimal",
db_type: "postgresql",
host: "localhost",
port: null,
username: null,
folder_id: null,
keychain_ref: null,
tag_ids: [],
favorite: false,
created_at: "2024-01-01T00:00:00Z",
updated_at: "2024-01-01T00:00:00Z",
};
expect(conn.ssh_host).toBeUndefined();
expect(conn.database).toBeUndefined();
});
});
describe("ConnectionInput", () => {
it("accepts ssh_password", () => {
const input: ConnectionInput = {
name: "x", db_type: "postgresql", host: "h", port: 5432, ssh_password: "ssh-pw",
};
expect(input.ssh_password).toBe("ssh-pw");
});
it("accepts all SSH/SSL fields", () => {
const input: ConnectionInput = {
name: "Test",
db_type: "postgresql",
host: "localhost",
port: 5432,
ssh_host: "bastion.example.com",
ssh_port: 22,
ssh_user: "tunnel",
ssh_auth_method: "key",
ssh_private_key_path: "/home/user/.ssh/id_rsa",
ssh_passphrase: "s3cret",
ssl_mode: "verify-full",
ssl_ca_path: "/etc/ssl/certs/ca.pem",
ssl_cert_path: "/etc/ssl/certs/client.crt",
ssl_key_path: "/etc/ssl/certs/client.key",
password: "dbpass",
database: "mydb",
};
expect(input.ssh_auth_method).toBe("key");
expect(input.ssl_mode).toBe("verify-full");
expect(input.ssh_passphrase).toBe("s3cret");
expect(input.database).toBe("mydb");
});
it("accepts password auth method", () => {
const input: ConnectionInput = {
name: "PW SSH",
db_type: "postgresql",
host: "db.example.com",
port: 5432,
ssh_host: "gateway.example.com",
ssh_port: 2222,
ssh_user: "proxy",
ssh_auth_method: "password",
};
expect(input.ssh_auth_method).toBe("password");
});
it("allows all SSH/SSL fields to be omitted", () => {
const input: ConnectionInput = {
name: "Simple",
db_type: "sqlite",
host: "localhost",
port: null,
};
expect(input.ssh_host).toBeUndefined();
expect(input.ssl_mode).toBeUndefined();
});
});
describe("TableInfo", () => {
it("has the correct shape", () => {
const table: TableInfo = {
name: "users",
schema: "public",
table_type: "TABLE",
};
expect(table.name).toBe("users");
expect(table.schema).toBe("public");
expect(table.table_type).toBe("TABLE");
});
it("accepts view type", () => {
const table: TableInfo = { name: "v1", schema: "public", table_type: "VIEW" };
expect(table.table_type).toBe("VIEW");
});
});
describe("ColumnInfo", () => {
it("has the correct shape", () => {
const col: ColumnInfo = {
name: "id",
data_type: "integer",
is_nullable: false,
is_pk: true,
is_fk: false,
fk_ref: null,
default_value: null,
editable: true,
is_generated: false,
};
expect(col.name).toBe("id");
expect(col.is_pk).toBe(true);
});
it("supports foreign key references", () => {
const col: ColumnInfo = {
name: "user_id",
data_type: "integer",
is_nullable: true,
is_pk: false,
is_fk: true,
fk_ref: ["users", "id"],
default_value: null,
editable: true,
is_generated: false,
};
expect(col.fk_ref?.[0]).toBe("users");
});
});
describe("QueryResult", () => {
it("is well-typed with columns and rows", () => {
const result: QueryResult = {
columns: [{name:"id",data_type:"text",is_pk:false,is_fk:false,is_nullable:false,default_value:null,fk_ref:null,editable:true,is_generated:false},{name:"name",data_type:"text",is_pk:false,is_fk:false,is_nullable:false,default_value:null,fk_ref:null,editable:true,is_generated:false},{name:"email",data_type:"text",is_pk:false,is_fk:false,is_nullable:false,default_value:null,fk_ref:null,editable:true,is_generated:false}],
rows: [
[1, "Alice"],
[2, "Bob"],
],
total_rows: 2, page: 1, page_size: 50,
execution_time_ms: 12.5,
};
expect(result.columns.length).toBe(3);
expect(result.total_rows).toBe(2);
expect(result.execution_time_ms).toBe(12.5);
});
it("allows error state", () => {
const result: QueryResult = {
columns: [],
rows: [],
total_rows: 0, page: 1, page_size: 50,
error: "Syntax error near FROM",
};
expect(result.error).toBe("Syntax error near FROM");
});
it("can have null execution_time", () => {
const result: QueryResult = {
columns: [{name:"id",data_type:"text",is_pk:false,is_fk:false,is_nullable:false,default_value:null,fk_ref:null,editable:true,is_generated:false}],
rows: [],
total_rows: 0, page: 1, page_size: 50,
execution_time_ms: null,
};
expect(result.execution_time_ms).toBeNull();
});
});
describe("ChangeItem", () => {
it("has a discriminated type", () => {
const createItem: ChangeItem = {
type: "create_table",
sql: "CREATE TABLE users (id INT)",
status: "pending",
id: "ch1",
description: "Create users table",
};
expect(createItem.type).toBe("create_table");
expect(createItem.status).toBe("pending");
const dropItem: ChangeItem = {
type: "drop_table",
sql: "DROP TABLE users",
status: "applied",
id: "ch2",
};
expect(dropItem.type).toBe("drop_table");
expect(dropItem.status).toBe("applied");
});
it("accepts error status with error message", () => {
const item: ChangeItem = {
type: "alter_table",
sql: "ALTER TABLE users ADD COLUMN age INT",
status: "error",
error: "Column already exists",
id: "ch3",
};
expect(item.status).toBe("error");
expect(item.error).toBe("Column already exists");
});
it("accepts all ChangeItemType values", () => {
const types: ChangeItemType[] = [
"create_table",
"alter_table",
"drop_table",
"insert",
"update",
"delete",
"create_index",
"drop_index",
];
const item: ChangeItem = {
type: types[0],
sql: "test",
status: "pending",
id: "ch4",
};
expect(types).toContain(item.type);
});
});
describe("ChangeStatus", () => {
it("accepts all status values", () => {
const statuses: ChangeStatus[] = ["pending", "applied", "error"];
expect(statuses).toHaveLength(3);
});
});
describe("DbViewerTab", () => {
it("can be created with required fields", () => {
const tab: DbViewerTab = {
id: "tab1",
connection_id: "c1",
title: "Query 1",
created_at: "2024-01-01T00:00:00Z",
updated_at: "2024-01-01T00:00:00Z",
};
expect(tab.title).toBe("Query 1");
expect(tab.query).toBeUndefined();
expect(tab.result).toBeUndefined();
expect(tab.changes).toBeUndefined();
});
it("can include query and result", () => {
const tab: DbViewerTab = {
id: "tab2",
connection_id: "c1",
title: "SELECT * FROM users",
query: "SELECT * FROM users",
result: {
columns: [{name:"id",data_type:"text",is_pk:false,is_fk:false,is_nullable:false,default_value:null,fk_ref:null,editable:true,is_generated:false},{name:"name",data_type:"text",is_pk:false,is_fk:false,is_nullable:false,default_value:null,fk_ref:null,editable:true,is_generated:false}],
rows: [],
total_rows: 0, page: 1, page_size: 50,
},
created_at: "2024-01-01T00:00:00Z",
updated_at: "2024-01-01T00:00:00Z",
};
expect(tab.query).toBe("SELECT * FROM users");
expect(tab.result?.total_rows).toBe(0);
});
it("can include changes array", () => {
const tab: DbViewerTab = {
id: "tab3",
connection_id: "c1",
title: "Migration",
changes: [
{ type: "create_table", sql: "CREATE TABLE t (id INT)", status: "pending", id: "ch1" },
],
created_at: "2024-01-01T00:00:00Z",
updated_at: "2024-01-01T00:00:00Z",
};
expect(tab.changes).toHaveLength(1);
expect(tab.changes![0].type).toBe("create_table");
});
});
describe("ConnectionTestResult", () => {
it("can represent a successful test", () => {
const result: ConnectionTestResult = {
ok: true,
server_version: "16.2",
latency_ms: 5,
};
expect(result.ok).toBe(true);
expect(result.server_version).toBe("16.2");
});
it("can represent a failed test with error", () => {
const result: ConnectionTestResult = {
ok: false,
error: "Connection refused",
};
expect(result.ok).toBe(false);
expect(result.error).toBe("Connection refused");
});
});
describe("Schema graph types", () => {
it("GraphColumn has correct shape", () => {
const col: GraphColumn = {
name: "user_id",
data_type: "integer",
is_pk: false,
is_fk: true,
is_unique: false,
is_nullable: false,
fk_ref: ["public", "users", "id"],
};
expect(col.name).toBe("user_id");
expect(col.is_fk).toBe(true);
expect(col.fk_ref).toEqual(["public", "users", "id"]);
});
it("TableNode has correct shape", () => {
const node: TableNode = {
name: "orders",
schema: "public",
table_type: "TABLE",
columns: [
{ name: "id", data_type: "integer", is_pk: true, is_fk: false, is_unique: true, is_nullable: false, fk_ref: null },
{ name: "user_id", data_type: "integer", is_pk: false, is_fk: true, is_unique: false, is_nullable: false, fk_ref: ["public", "users", "id"] },
],
};
expect(node.name).toBe("orders");
expect(node.columns).toHaveLength(2);
});
it("Relationship has correct shape", () => {
const rel: Relationship = {
source_schema: "public",
source_table: "orders",
source_column: "user_id",
target_schema: "public",
target_table: "users",
target_column: "id",
cardinality: "1:N",
};
expect(rel.cardinality).toBe("1:N");
});
it("SchemaGraph has correct shape", () => {
const graph: SchemaGraph = {
tables: [
{ name: "users", schema: "public", table_type: "TABLE", columns: [] },
],
relationships: [],
};
expect(graph.tables).toHaveLength(1);
expect(graph.relationships).toHaveLength(0);
});
it("fk_ref can be null for non-FK columns", () => {
const col: GraphColumn = {
name: "name",
data_type: "text",
is_pk: false,
is_fk: false,
is_unique: false,
is_nullable: true,
fk_ref: null,
};
expect(col.fk_ref).toBeNull();
});
});
describe("Settings", () => {
it("includes the five editor option fields", () => {
const s: Settings = {
confirm_before_delete: true, default_folder_id: null, theme: "dark",
font_size: "medium", default_ports: {}, tag_order: null, table_refresh_rate: 0,
table_page_size: 50, shortcuts: {}, accent_color: "#2563EB",
editor_font_size: 13, editor_font_family: "Space Mono",
editor_word_wrap: "off", editor_minimap: false, editor_tab_size: 4,
};
expect(s.editor_font_size).toBe(13);
expect(s.editor_word_wrap).toBe("off");
});
});
describe("v0.5.0 types", () => {
it("ColumnInfo carries editability metadata", () => {
const c: ColumnInfo = {
name: "id", data_type: "integer", is_nullable: false, is_pk: true,
is_fk: false, fk_ref: null, default_value: null, editable: false, is_generated: false,
};
expectTypeOf(c.editable).toEqualTypeOf<boolean>();
expectTypeOf(c.is_generated).toEqualTypeOf<boolean>();
});
it("IndexInfo has the documented fields", () => {
const i: IndexInfo = {
name: "idx", schema: "public", table: "users", definition: "CREATE INDEX ...",
is_unique: true, method: "btree", columns: ["id"], size_bytes: 4096, tablespace: null,
};
expectTypeOf(i).toMatchTypeOf<IndexInfo>();
});
it("ConstraintInfo has the documented fields", () => {
const c: ConstraintInfo = {
name: "ck", schema: "public", table: "users", contype: "CHECK",
definition: "CHECK (x > 0)", deferrable: false, validated: true, columns: ["x"],
};
expectTypeOf(c).toMatchTypeOf<ConstraintInfo>();
});
it("RecentConnection pairs a connection id with an opened_at timestamp", () => {
const r: RecentConnection = { connection_id: "c1", opened_at: "2026-08-02T00:00:00Z" };
expectTypeOf(r.connection_id).toEqualTypeOf<string>();
});
it("Connection carries favorite", () => {
const c = { favorite: true } as Connection;
expectTypeOf(c.favorite).toEqualTypeOf<boolean>();
});
});