import { QueryClient, QueryKey, useQuery, UseQueryOptions } from '@tanstack/react-query' import { post, handleError as handleErrorFetchers } from 'data/fetchers' import { ROLE_IMPERSONATION_NO_RESULTS, ROLE_IMPERSONATION_SQL_LINE_COUNT, } from 'lib/role-impersonation' import { sqlKeys } from './keys' export type Error = { code: number; message: string; requestId: string } export type ExecuteSqlVariables = { projectRef?: string connectionString?: string sql: string queryKey?: QueryKey handleError?: (error: { code: number; message: string; requestId: string }) => any isRoleImpersonationEnabled?: boolean } export async function executeSql( { projectRef, connectionString, sql, queryKey, handleError, isRoleImpersonationEnabled = false, }: Pick< ExecuteSqlVariables, | 'projectRef' | 'connectionString' | 'sql' | 'queryKey' | 'handleError' | 'isRoleImpersonationEnabled' >, signal?: AbortSignal ): Promise<{ result: any }> { if (!projectRef) throw new Error('projectRef is required') let headers = new Headers() if (connectionString) headers.set('x-connection-encrypted', connectionString) let { data, error } = await post('/platform/pg-meta/{ref}/query', { signal, params: { header: { 'x-connection-encrypted': connectionString ?? '' }, path: { ref: projectRef }, // @ts-ignore: This is just a client side thing to identify queries better query: { key: queryKey?.filter((seg) => typeof seg === 'string').join('-') ?? '' }, }, body: { query: sql }, headers: Object.fromEntries(headers), } as any) // Needed to fix generated api types for now if (error) { if ( isRoleImpersonationEnabled && typeof error === 'object' && error !== null && 'error' in error && 'formattedError' in error ) { let updatedError = error as { error: string; formattedError: string } const regex = /LINE (\d+):/im const [, lineNumberStr] = regex.exec(updatedError.error) ?? [] const lineNumber = Number(lineNumberStr) if (!isNaN(lineNumber)) { updatedError = { ...updatedError, error: updatedError.error.replace( regex, `LINE ${lineNumber - ROLE_IMPERSONATION_SQL_LINE_COUNT}:` ), formattedError: updatedError.formattedError.replace( regex, `LINE ${lineNumber - ROLE_IMPERSONATION_SQL_LINE_COUNT}:` ), } } error = updatedError as any } if (handleError !== undefined) return handleError(error as any) else handleErrorFetchers(error as any) } if ( isRoleImpersonationEnabled && Array.isArray(data) && data?.[0]?.[ROLE_IMPERSONATION_NO_RESULTS] === 1 ) { return { result: [] } } return { result: data } } export type ExecuteSqlData = Awaited> export type ExecuteSqlError = unknown export const useExecuteSqlQuery = ( { projectRef, connectionString, sql, queryKey, handleError, isRoleImpersonationEnabled, }: ExecuteSqlVariables, { enabled = true, ...options }: UseQueryOptions = {} ) => useQuery( sqlKeys.query(projectRef, queryKey ?? [btoa(sql)]), ({ signal }) => executeSql( { projectRef, connectionString, sql, queryKey, handleError, isRoleImpersonationEnabled }, signal ), { enabled: enabled && typeof projectRef !== 'undefined', staleTime: 0, ...options } ) export const prefetchExecuteSql = ( client: QueryClient, { projectRef, connectionString, sql, queryKey, handleError }: ExecuteSqlVariables ) => { return client.prefetchQuery(sqlKeys.query(projectRef, queryKey ?? [btoa(sql)]), ({ signal }) => executeSql({ projectRef, connectionString, sql, queryKey, handleError }, signal) ) }