Let a super admin read the database from the app

A new super admin settings page, /admin/settings/database, lists every
table with its record count and size, and one page of the open table's
records newest key first. Values are cut inside SQL (400 characters, with
a flag saying so) and a record opens whole in a modal, so a table of long
texts reads without shipping it all. A search matches every column's
text, word by word or as a phrase, which is the one thing that also works
on a virtual table. Nothing in it writes: the catalog, the records and
the single record are all reads, GET only.
This commit is contained in:
2026-10-23 06:03:42 +01:00
parent 9b26c169d6
commit 41bb4aaac2
48 changed files with 2573 additions and 0 deletions
@@ -0,0 +1,59 @@
import BunSQLite from "@moduletrace/bun-sqlite";
import quoteSqlIdentifier from "./quote-sql-identifier";
import resolveTableKey from "./resolve-table-key";
import type { DatabaseTableSummary } from "@/src/types";
import type { TableCatalogEntry } from "./table-catalog-types";
type Params = {
tables: TableCatalogEntry[];
/**
* How many bytes each table takes on disk, by name; empty when SQLite
* can't measure it, which leaves the size column blank.
*/
sizes: Map<string, number>;
};
type CountRow = { rows: number };
/**
* The catalog as the browser lists it: every table with its record count,
* its size on disk, its columns and what identifies its records.
*
* Each count is its own tiny statement rather than one aggregate, because
* a virtual table has no rowid to build one from, and the whole read stays
* in the low milliseconds for the install's fifty-odd tables.
*/
export default async function buildDatabaseTables({ tables, sizes }: Params): Promise<DatabaseTableSummary[]> {
const summaries: DatabaseTableSummary[] = [];
for (const table of tables) {
const size = sizes.get(table.name);
summaries.push({
name: table.name,
type: table.type,
rows: await countRows({ table: table.name }),
size_bytes: size === undefined ? null : size,
columns: table.columns,
key: resolveTableKey({ columns: table.columns, has_rowid: table.has_rowid }),
});
}
return summaries;
}
async function countRows({ table }: { table: string }): Promise<number | null> {
const res = await BunSQLite.sql<CountRow>({
sql: `SELECT COUNT(*) AS rows FROM ${quoteSqlIdentifier({ name: table })}`,
});
if (!res.success || !res.payload?.[0]) {
/**
* A table the module won't count (an index over a table whose
* module is missing) is still a table; it just has no count.
*/
return null;
}
return Number(res.payload[0].rows) || 0;
}