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:
@@ -0,0 +1,89 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import buildRecordRow from "./build-record-row";
|
||||
import buildRecordsSql, { buildCountSql } from "./build-records-sql";
|
||||
import clampPage from "./clamp-page";
|
||||
import grabTableCatalogEntry from "./grab-table-catalog-entry";
|
||||
import type { DatabaseRecord, DatabaseRecordsPayload, DatabaseSearchMode } from "@/src/types";
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
|
||||
type Params = {
|
||||
table?: string | null;
|
||||
/**
|
||||
* Which page of records, from 1.
|
||||
*/
|
||||
page?: number;
|
||||
/**
|
||||
* What to look for in the table's records, across every column's text.
|
||||
*/
|
||||
search?: string;
|
||||
mode?: DatabaseSearchMode;
|
||||
};
|
||||
|
||||
type TotalRow = { total: number };
|
||||
|
||||
/**
|
||||
* One page of one table's records, read-only: the table asked for with all
|
||||
* of its columns, its records newest key first, and how many matched so the
|
||||
* pager knows where the end is.
|
||||
*
|
||||
* Every value crosses out of SQLite already cut to
|
||||
* `AppData.DatabaseBrowserCellMaxChars`, so a table of long texts can't
|
||||
* turn one page into a megabyte of JSON.
|
||||
*/
|
||||
export default async function grabTableRecords({
|
||||
table,
|
||||
page = 1,
|
||||
search = "",
|
||||
mode = "every_word",
|
||||
}: Params): Promise<DatabaseRecordsPayload> {
|
||||
const entry = await grabTableCatalogEntry({ table });
|
||||
const page_size = AppData.DatabaseBrowserPageSize;
|
||||
const current_page = Math.max(1, Math.trunc(page || 1));
|
||||
const columns = entry.columns.map((column) => column.name);
|
||||
const trimmed = search.trim();
|
||||
const search_params = trimmed
|
||||
? { text: trimmed, mode, max_words: AppData.DatabaseBrowserMaxSearchWords }
|
||||
: null;
|
||||
const count_res = await BunSQLite.sql<TotalRow>(
|
||||
buildCountSql({
|
||||
table: entry.name,
|
||||
columns,
|
||||
search: search_params,
|
||||
}),
|
||||
);
|
||||
const total = Number(count_res.payload?.[0]?.total) || 0;
|
||||
const final_page = clampPage({ page: current_page, total, page_size });
|
||||
const page_query = buildRecordsSql({
|
||||
table: entry.name,
|
||||
columns,
|
||||
key: entry.key,
|
||||
max_chars: AppData.DatabaseBrowserCellMaxChars,
|
||||
limit: page_size,
|
||||
offset: (final_page - 1) * page_size,
|
||||
search: search_params,
|
||||
});
|
||||
const res = await BunSQLite.sql<Record<string, unknown>>(page_query);
|
||||
|
||||
if (!res.success || !res.payload) {
|
||||
throw new AppError(`Couldn't read the records of "${entry.name}".`);
|
||||
}
|
||||
|
||||
const records: DatabaseRecord[] = res.payload.map((row) =>
|
||||
buildRecordRow({
|
||||
row,
|
||||
columns: entry.columns,
|
||||
max_chars: AppData.DatabaseBrowserCellMaxChars,
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
table: entry,
|
||||
records,
|
||||
total,
|
||||
page: final_page,
|
||||
page_size,
|
||||
search: trimmed,
|
||||
mode,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user