fix(database-view): send tableId to bridge and correct SSE path

useViewData omitted the tableId query param required by the bridge
GET /views/:id/data route -> 400 'tableId query param required' and a
blank 'Could not load view'. The SSE consumer hit /api/v1/events/sse
but the bridge mounts the stream at /api/events -> 404 reconnect loop.
Thread tableId through ViewDataParams and all five callers; point the
SSE URL at /api/events.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Corentin JOGUET 2026-05-18 09:20:38 +00:00
parent a23f836358
commit fe75ea5c45
8 changed files with 18 additions and 4 deletions

View file

@ -6,7 +6,7 @@ import { resolveBridgeUrl } from "../services/bridge-client";
/** /**
* SSE consumer for realtime row/view updates from the bridge. * SSE consumer for realtime row/view updates from the bridge.
* *
* Connects to `GET /api/v1/events/sse?tables=<tableId>&views=<viewId>` and * Connects to `GET /api/events/sse?tables=<tableId>&views=<viewId>` and
* listens for events whose type starts with `row.` or `view.`. On match, it * listens for events whose type starts with `row.` or `view.`. On match, it
* invalidates the React Query cache for the affected view so the table * invalidates the React Query cache for the affected view so the table
* re-fetches silently. * re-fetches silently.
@ -46,7 +46,10 @@ export function useDatabaseRealtimeUpdates(
if (!tableId || !viewId) return; if (!tableId || !viewId) return;
const url = resolveBridgeUrl(bridgeUrl); const url = resolveBridgeUrl(bridgeUrl);
const sseUrl = `${url}/api/v1/events/sse?tables=${encodeURIComponent(tableId)}&views=${encodeURIComponent(viewId)}`; // The bridge mounts the SSE router at /api/events (NOT under /api/v1) on
// purpose, to keep it out of the v1 mutation rate-limiter. See bridge
// src/index.ts: app.route('/api', eventsRouter) + eventsRoutes '/events'.
const sseUrl = `${url}/api/events/sse?tables=${encodeURIComponent(tableId)}&views=${encodeURIComponent(viewId)}`;
let retryTimeout: ReturnType<typeof setTimeout> | null = null; let retryTimeout: ReturnType<typeof setTimeout> | null = null;

View file

@ -34,6 +34,7 @@ export interface UseViewDataResult {
*/ */
export function useViewData({ export function useViewData({
viewId, viewId,
tableId,
bridgeUrl, bridgeUrl,
page = 1, page = 1,
size = 50, size = 50,
@ -42,11 +43,13 @@ export function useViewData({
return useQuery<UseViewDataResult>({ return useQuery<UseViewDataResult>({
queryKey: viewDataQueryKey(viewId, page, size, url), queryKey: viewDataQueryKey(viewId, page, size, url),
enabled: Boolean(viewId), enabled: Boolean(viewId) && Boolean(tableId),
queryFn: async () => { queryFn: async () => {
const client = getBridgeClient(url); const client = getBridgeClient(url);
// tableId is mandatory: the bridge route GET /views/:viewId/data returns
// 400 "tableId query param required" without it.
const res = await (client.get(`/api/v1/views/${viewId}/data`, { const res = await (client.get(`/api/v1/views/${viewId}/data`, {
params: { page, size }, params: { page, size, tableId },
}) as unknown as Promise<BridgeViewDataResponse>); }) as unknown as Promise<BridgeViewDataResponse>);
// Normalise: bridge may return top-level array or wrapped envelope. // Normalise: bridge may return top-level array or wrapped envelope.

View file

@ -102,6 +102,7 @@ export function CalendarRenderer({ tableId, viewId, bridgeUrl }: CalendarRendere
const { data, isLoading, isError, error, refetch } = useViewData({ const { data, isLoading, isError, error, refetch } = useViewData({
viewId, viewId,
tableId,
bridgeUrl, bridgeUrl,
page: 1, page: 1,
size: PAGE_SIZE, size: PAGE_SIZE,

View file

@ -280,6 +280,7 @@ export function KanbanRenderer({ tableId, viewId, bridgeUrl }: KanbanRendererPro
const { data, isLoading, isError, error, refetch } = useViewData({ const { data, isLoading, isError, error, refetch } = useViewData({
viewId, viewId,
tableId,
bridgeUrl, bridgeUrl,
page: 1, page: 1,
size: PAGE_SIZE, size: PAGE_SIZE,

View file

@ -173,6 +173,7 @@ export function TableRenderer({ tableId, viewId, bridgeUrl }: TableRendererProps
const { data, isLoading, isError, error, refetch } = useViewData({ const { data, isLoading, isError, error, refetch } = useViewData({
viewId, viewId,
tableId,
bridgeUrl, bridgeUrl,
page, page,
size: PAGE_SIZE, size: PAGE_SIZE,

View file

@ -261,6 +261,7 @@ export function TimelineRenderer({ tableId, viewId, bridgeUrl }: TimelineRendere
const { data, isLoading: dataLoading, isError: dataError, error, refetch } = useViewData({ const { data, isLoading: dataLoading, isError: dataError, error, refetch } = useViewData({
viewId, viewId,
tableId,
bridgeUrl, bridgeUrl,
page: 1, page: 1,
size: PAGE_SIZE, size: PAGE_SIZE,

View file

@ -118,6 +118,7 @@ export function InsertDatabaseModal({
isError: fieldsError, isError: fieldsError,
} = useViewData({ } = useViewData({
viewId: selectedView?.id ?? "", viewId: selectedView?.id ?? "",
tableId: selectedTable?.id ?? "",
bridgeUrl, bridgeUrl,
page: 1, page: 1,
size: 1, size: 1,

View file

@ -71,6 +71,9 @@ export interface BridgeViewDataResponse {
/** Paginated fetch params for the view-data hook. */ /** Paginated fetch params for the view-data hook. */
export interface ViewDataParams { export interface ViewDataParams {
viewId: string; viewId: string;
// Required by the bridge: GET /views/:viewId/data needs tableId to build
// Row instances (Baserow does not echo the tableId in listRows responses).
tableId: string;
bridgeUrl?: string | null; bridgeUrl?: string | null;
page?: number; page?: number;
size?: number; size?: number;