From 978c6b684b38f030ec625e90a8653bb8612ce4e4 Mon Sep 17 00:00:00 2001 From: dal Date: Mon, 18 Aug 2025 13:54:43 -0600 Subject: [PATCH] Add report files proxy router and integrate with existing API --- .../v2/electric-shape/_proxyRouterConfig.ts | 4 +- .../src/api/v2/electric-shape/report-files.ts | 130 ++++++++++++++++++ 2 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 apps/server/src/api/v2/electric-shape/report-files.ts diff --git a/apps/server/src/api/v2/electric-shape/_proxyRouterConfig.ts b/apps/server/src/api/v2/electric-shape/_proxyRouterConfig.ts index 27e89999d..a28161491 100644 --- a/apps/server/src/api/v2/electric-shape/_proxyRouterConfig.ts +++ b/apps/server/src/api/v2/electric-shape/_proxyRouterConfig.ts @@ -1,8 +1,9 @@ import type { Context } from 'hono'; import { chatsProxyRouter } from './chats'; import { messagesProxyRouter } from './messages'; +import { reportFilesProxyRouter } from './report-files'; -type SupportedTables = 'messages' | 'chats'; +type SupportedTables = 'messages' | 'chats' | 'report_files'; const proxyRouter: Record< SupportedTables, @@ -10,6 +11,7 @@ const proxyRouter: Record< > = { messages: messagesProxyRouter, chats: chatsProxyRouter, + report_files: reportFilesProxyRouter, }; export default proxyRouter; diff --git a/apps/server/src/api/v2/electric-shape/report-files.ts b/apps/server/src/api/v2/electric-shape/report-files.ts new file mode 100644 index 000000000..c5a9a98ab --- /dev/null +++ b/apps/server/src/api/v2/electric-shape/report-files.ts @@ -0,0 +1,130 @@ +import { markdownToPlatejs } from '@buster/server-utils/report'; +import type { Context } from 'hono'; +import { errorResponse } from '../../../utils/response'; +import { createProxiedResponse, extractParamFromWhere } from './_helpers'; + +// Types for Electric SQL response format +interface ElectricDataRow { + value: T; + key: string; + headers: { + last?: boolean; + relation: string[]; + operation: string; + lsn: string; + op_position: number; + txids: number[]; + }; +} + +interface ElectricControlMessage { + headers: { + control: string; + global_last_seen_lsn?: string; + }; +} + +type ElectricResponse = Array | ElectricControlMessage>; + +// Type for report_files table row +interface ReportFileRow { + id: string; + name: string; + content: string; // This is what we'll transform + organization_id: string; + created_by: string; + created_at: string; + updated_at: string; + deleted_at?: string | null; + publicly_accessible: boolean; + publicly_enabled_by?: string | null; + public_expiry_date?: string | null; + version_history: Record< + string, + { + content: string; + updated_at: string; + version_number: number; + } + >; + public_password?: string | null; + workspace_sharing: string; + workspace_sharing_enabled_by?: string | null; + workspace_sharing_enabled_at?: string | null; +} + +/** + * Transform the content field from markdown to PlateJS format + */ +export async function transformReportFilesResponse(response: Response): Promise { + const data = (await response.json()) as ElectricResponse; + + // Transform content field for data rows + const transformedData = await Promise.all( + data.map(async (item) => { + // Type guard to check if this is a data row (not a control message) + if ('value' in item && item.value.content) { + const { elements, error } = await markdownToPlatejs(item.value.content); + + if (error) { + console.error('Error transforming report content:', error); + // Keep original content if transformation fails + return item; + } + + // Replace content with PlateJS elements + return { + ...item, + value: { + ...item.value, + content: JSON.stringify(elements), + }, + }; + } + + // Return control messages and other data unchanged + return item; + }) + ); + + // Return new response with same headers + return new Response(JSON.stringify(transformedData), { + headers: response.headers, + status: response.status, + statusText: response.statusText, + }); +} + +export const reportFilesProxyRouter = async (url: URL, _userId: string, c: Context) => { + const reportId = extractParamFromWhere(url, 'id'); + + if (!reportId) { + throw errorResponse('Report ID (id) is required', 403); + } + + // TODO: Implement proper access control for reports + // Should check: + // 1. If user created the report (createdBy === userId) + // 2. If user is in the same organization as the report + // 3. If workspace sharing is enabled ('can_view' or 'can_edit') + // 4. If report has individual permissions for the user + // 5. If report is publicly accessible + // + // const userHasAccess = await canUserAccessReport({ + // userId: c.get('supabaseUser').id, + // reportId, + // }); + // + // if (!userHasAccess) { + // throw errorResponse('You do not have access to this report', 403); + // } + + // For now, allow access with a warning + console.warn( + `TODO: Implement access control for report ${reportId} for user ${c.get('supabaseUser').id}` + ); + + // Fetch the response and transform it + const response = await createProxiedResponse(url); + return await transformReportFilesResponse(response); +};