Advertisement

← vibesdk

src/routes/chat/hooks/use-database-viewer.ts

5.2 KB Download file
/**
 * use-database-viewer
 *
 * Lightweight data hook for the DB tab. Manages two pieces of state:
 * - the list of tables in the App Durable Object,
 * - the paginated rows of the selected table.
 *
 * Fetches are manual (refresh button); the hook never polls.
 */
import { useCallback, useEffect, useRef, useState } from 'react';
import { apiClient } from '@/lib/api-client';
import type {
	ListAppTablesResponse,
	QueryAppTableResponse,
} from '@/api-types';

export interface DatabaseViewerState {
	tables: ListAppTablesResponse | null;
	queryResult: QueryAppTableResponse | null;
	loadingTables: boolean;
	loadingQuery: boolean;
	error: string | null;
	selectedTable: string | null;
	page: number;
	pageSize: number;
	orderBy?: string;
	orderDir?: 'asc' | 'desc';
}

export interface DatabaseViewerActions {
	refreshTables: () => Promise<void>;
	refreshQuery: () => Promise<void>;
	selectTable: (table: string | null) => void;
	setPage: (page: number) => void;
	setPageSize: (size: number) => void;
	setOrder: (orderBy: string | undefined, orderDir?: 'asc' | 'desc') => void;
	wipe: () => Promise<void>;
}

const DEFAULT_PAGE_SIZE = 50;
const MAX_PAGE_SIZE = 200;

export function useDatabaseViewer(
	agentId: string | undefined,
	options: { enabled: boolean },
): DatabaseViewerState & DatabaseViewerActions {
	const enabled = options.enabled && !!agentId;

	const [tables, setTables] = useState<ListAppTablesResponse | null>(null);
	const [queryResult, setQueryResult] = useState<QueryAppTableResponse | null>(null);
	const [loadingTables, setLoadingTables] = useState(false);
	const [loadingQuery, setLoadingQuery] = useState(false);
	const [error, setError] = useState<string | null>(null);
	const [selectedTable, setSelectedTable] = useState<string | null>(null);
	const [page, setPage] = useState(0);
	const [pageSize, setPageSizeRaw] = useState(DEFAULT_PAGE_SIZE);
	const [orderBy, setOrderBy] = useState<string | undefined>(undefined);
	const [orderDir, setOrderDir] = useState<'asc' | 'desc' | undefined>(undefined);

	const queryReqIdRef = useRef(0);
	const tablesReqIdRef = useRef(0);

	const setPageSize = useCallback((size: number) => {
		setPageSizeRaw(Math.max(1, Math.min(MAX_PAGE_SIZE, size)));
		setPage(0);
	}, []);

	const setOrder = useCallback((by: string | undefined, dir?: 'asc' | 'desc') => {
		setOrderBy(by);
		setOrderDir(dir);
		setPage(0);
	}, []);

	const refreshTables = useCallback(async () => {
		if (!enabled || !agentId) return;
		const reqId = ++tablesReqIdRef.current;
		setLoadingTables(true);
		setError(null);
		try {
			const res = await apiClient.listAppTables(agentId);
			if (reqId !== tablesReqIdRef.current) return;
			if (res.success && res.data) {
				setTables(res.data);
			} else {
				setTables(null);
				setError(res.error?.message ?? 'Failed to load tables');
			}
		} catch (e) {
			if (reqId !== tablesReqIdRef.current) return;
			setError(e instanceof Error ? e.message : String(e));
		} finally {
			if (reqId === tablesReqIdRef.current) setLoadingTables(false);
		}
	}, [agentId, enabled]);

	const refreshQuery = useCallback(async () => {
		if (!enabled || !agentId || !selectedTable) {
			setQueryResult(null);
			return;
		}
		const reqId = ++queryReqIdRef.current;
		setLoadingQuery(true);
		try {
			const res = await apiClient.queryAppTable(agentId, {
				table: selectedTable,
				limit: pageSize,
				offset: page * pageSize,
				orderBy,
				orderDir,
			});
			if (reqId !== queryReqIdRef.current) return;
			if (res.success && res.data) {
				setQueryResult(res.data);
			} else {
				setQueryResult(null);
				setError(res.error?.message ?? 'Failed to load rows');
			}
		} catch (e) {
			if (reqId !== queryReqIdRef.current) return;
			setError(e instanceof Error ? e.message : String(e));
		} finally {
			if (reqId === queryReqIdRef.current) setLoadingQuery(false);
		}
	}, [agentId, enabled, selectedTable, pageSize, page, orderBy, orderDir]);

	const selectTable = useCallback((table: string | null) => {
		setSelectedTable(table);
		setPage(0);
		setOrderBy(undefined);
		setOrderDir(undefined);
		setQueryResult(null);
	}, []);

	const wipe = useCallback(async () => {
		if (!enabled || !agentId) return;
		setLoadingQuery(true);
		try {
			const res = await apiClient.wipeAppDatabase(agentId);
			if (!res.success) {
				setError(res.error?.message ?? 'Wipe failed');
				return;
			}
			setQueryResult(null);
			setTables(null);
			setSelectedTable(null);
			await refreshTables();
		} catch (e) {
			setError(e instanceof Error ? e.message : String(e));
		} finally {
			setLoadingQuery(false);
		}
	}, [agentId, enabled, refreshTables]);

	// Initial + on-enabled-toggle fetch.
	useEffect(() => {
		if (enabled) {
			void refreshTables();
		}
	}, [enabled, refreshTables]);

	// Refresh query when paging / table / order changes.
	useEffect(() => {
		if (enabled && selectedTable) {
			void refreshQuery();
		}
	}, [enabled, selectedTable, page, pageSize, orderBy, orderDir, refreshQuery]);

	return {
		tables,
		queryResult,
		loadingTables,
		loadingQuery,
		error,
		selectedTable,
		page,
		pageSize,
		orderBy,
		orderDir,
		refreshTables,
		refreshQuery,
		selectTable,
		setPage,
		setPageSize,
		setOrder,
		wipe,
	};
}