mirror of
https://github.com/supabase/supabase.git
synced 2026-09-09 03:19:36 +08:00
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Feature (data layer only — PR 1 of the SQL-editor query-source stack; nothing user-visible yet, no consumers). ## What is the current behavior? The Studio SQL editor only runs queries against Postgres. There is no type-safe brand for user-authored logs SQL and no execution/normalization layer for running SQL against the logs/analytics (ClickHouse) backend. ## What is the new behavior? Pure additions, no behavior change: - `data/logs/safe-analytics-sql.ts` — adds distinct untrusted/safe brands for user-authored logs SQL (`UntrustedLogSqlFragment`, `untrustedLogSql`, `acceptUntrustedLogsSql`), mirroring pg-meta's `UntrustedSqlFragment` but kept intentionally disjoint so Postgres and logs SQL can never cross boundaries. - `data/logs/execute-logs-sql-mutation.ts` (new) — `executeLogsSql` wraps `executeAnalyticsSql`, attaches the resolved time range as request params (`iso_timestamp_start/end`, never spliced into SQL), and normalizes to `{ rows, error? }`; `mapLogsError` normalizes the analytics backend's structured 200-body error into the `{ message }` shape the result pane reads; `useExecuteLogsSqlMutation` collapses transport and 200-body errors into React Query's single `onError` path. - Unit tests for `mapLogsError`, the brands (including compile-time disjointness vs pg-meta brands), and safe composition. Verification: `pnpm test:studio` (new suites, 26 passed), `pnpm typecheck`, `lint:ratchet` (no new warnings), and Prettier all pass. ## Additional context <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added the ability to run user-authored logs SQL with resolved start/end timestamps. * Normalized query error handling so failures surface a clear message (including sensible fallbacks) and integrates with mutation error flows (with a default error toast when not customized). * Introduced safety branding for logs SQL fragments, including promotion to runnable safe SQL. * **Tests** * Added tests covering error normalization across multiple malformed/empty error shapes. * Added tests ensuring logs SQL branding preserves/accepts only the intended types and rejects unsafe inputs. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
177 lines
6.3 KiB
TypeScript
177 lines
6.3 KiB
TypeScript
/**
|
|
* Execution data layer for user-authored logs SQL run from the SQL editor.
|
|
*
|
|
* This is the logs-path analog of `data/sql/execute-sql-mutation.ts`. It wraps
|
|
* the analytics wire boundary (`executeAnalyticsSql`) and:
|
|
* - accepts only a `SafeLogSqlFragment` (promoted from `UntrustedLogSqlFragment`
|
|
* at an explicit user run gesture — see `safe-analytics-sql.ts`),
|
|
* - attaches the resolved time range as request params
|
|
* (`iso_timestamp_start`/`iso_timestamp_end`), never spliced into the SQL,
|
|
* - normalizes the response to `{ rows, error? }`.
|
|
*
|
|
* There are two ways a logs query can fail, and the layer keeps them distinct
|
|
* only at the pure-function level:
|
|
* - Transport failures (non-2xx) throw out of `executeAnalyticsSql` via
|
|
* `handleError`.
|
|
* - The analytics backend also returns a *200 body* carrying a structured error
|
|
* (`{ code, errors, message, status }`) — the same shape `useLogsQuery`
|
|
* reads. `mapLogsError` normalizes that into the `{ message }` shape the SQL
|
|
* editor's result pane renders, returned in `executeLogsSql`'s `error` field.
|
|
*
|
|
* `useExecuteLogsSqlMutation` then collapses both into React Query's single
|
|
* `onError` path so consumers handle one normalized error shape in one place.
|
|
*/
|
|
import { useMutation } from '@tanstack/react-query'
|
|
import { toast } from 'sonner'
|
|
|
|
import { executeAnalyticsSql, type AnalyticsSqlEndpoint } from './execute-analytics-sql'
|
|
import type { SafeLogSqlFragment } from './safe-analytics-sql'
|
|
import type { ResolvedLogDateRange } from '@/components/interfaces/Settings/Logs/logsDateRange'
|
|
import type { UseCustomMutationOptions } from '@/types'
|
|
|
|
/**
|
|
* Normalized logs query error in the shape the SQL editor result pane reads
|
|
* (`{ message }`). Produced by `mapLogsError` from either a transport error or
|
|
* the analytics backend's structured 200-body error.
|
|
*/
|
|
export interface LogsQueryError {
|
|
message: string
|
|
}
|
|
|
|
/**
|
|
* Structured error the analytics backend returns inside a 200 response body.
|
|
* Mirrors `LFResponse['error']` from the Logs types; every field is optional
|
|
* here because the value crosses the wire and must be treated defensively.
|
|
*/
|
|
interface RawLogsResponseError {
|
|
code?: number
|
|
errors?: Array<{ domain?: string; message?: string; reason?: string }>
|
|
message?: string
|
|
status?: string
|
|
}
|
|
|
|
const UNKNOWN_LOGS_ERROR_MESSAGE = 'An unexpected error occurred while running the logs query.'
|
|
|
|
/**
|
|
* Normalizes an analytics backend error into the `{ message }` shape the SQL
|
|
* editor result pane renders. Follows `useLogsQuery`'s extraction: prefer the
|
|
* top-level `message`, then the first nested `errors[].message`. Returns
|
|
* `undefined` when there is no error, and a generic fallback message when an
|
|
* error is present but carries no usable text.
|
|
*/
|
|
export function mapLogsError(error: unknown): LogsQueryError | undefined {
|
|
if (error === undefined || error === null) return undefined
|
|
|
|
if (typeof error === 'string') {
|
|
return { message: error.length > 0 ? error : UNKNOWN_LOGS_ERROR_MESSAGE }
|
|
}
|
|
|
|
if (typeof error === 'object') {
|
|
const structured = error as RawLogsResponseError
|
|
const nestedMessage = Array.isArray(structured.errors)
|
|
? structured.errors.find(
|
|
(entry) => typeof entry?.message === 'string' && entry.message.length > 0
|
|
)?.message
|
|
: undefined
|
|
const message = structured.message || nestedMessage
|
|
if (typeof message === 'string' && message.length > 0) {
|
|
return { message }
|
|
}
|
|
}
|
|
|
|
return { message: UNKNOWN_LOGS_ERROR_MESSAGE }
|
|
}
|
|
|
|
export interface ExecuteLogsSqlVariables {
|
|
projectRef: string
|
|
/** Must carry the `SafeLogSqlFragment` brand — promote at the run gesture. */
|
|
sql: SafeLogSqlFragment
|
|
/** Resolved (absolute) time range; relative ranges re-resolve at each run. */
|
|
range: ResolvedLogDateRange
|
|
/**
|
|
* Analytics endpoint to run against. The SQL editor pins this to the OTEL
|
|
* (ClickHouse) endpoint; kept as a param so callers stay explicit.
|
|
*/
|
|
endpoint: AnalyticsSqlEndpoint
|
|
signal?: AbortSignal
|
|
}
|
|
|
|
export interface ExecuteLogsSqlResult {
|
|
rows: unknown[]
|
|
/** Present when the backend returned a 200 body carrying a structured error. */
|
|
error?: LogsQueryError
|
|
}
|
|
|
|
/**
|
|
* Runs a logs SQL query against the analytics backend and normalizes the
|
|
* response. Transport failures throw; a structured 200-body error is returned
|
|
* in `error` alongside empty `rows`.
|
|
*
|
|
* @throws {ResponseError} on transport failure (via `executeAnalyticsSql`).
|
|
*/
|
|
export async function executeLogsSql({
|
|
projectRef,
|
|
sql,
|
|
range,
|
|
endpoint,
|
|
signal,
|
|
}: ExecuteLogsSqlVariables): Promise<ExecuteLogsSqlResult> {
|
|
const data = await executeAnalyticsSql({
|
|
projectRef,
|
|
endpoint,
|
|
sql,
|
|
iso_timestamp_start: range.from,
|
|
iso_timestamp_end: range.to,
|
|
key: 'sql-editor',
|
|
signal,
|
|
})
|
|
|
|
const body = (data ?? {}) as { result?: unknown[]; error?: unknown }
|
|
const error = mapLogsError(body.error)
|
|
|
|
return { rows: body.result ?? [], ...(error ? { error } : {}) }
|
|
}
|
|
|
|
/**
|
|
* React Query mutation wrapping `executeLogsSql`.
|
|
*
|
|
* Collapses both failure modes into React Query's single `onError` path: a
|
|
* transport failure (thrown by `executeAnalyticsSql`) and the analytics
|
|
* backend's structured 200-body error are both normalized to `LogsQueryError`
|
|
* before they reach `onError`, so callers only ever handle one error shape in
|
|
* one place. `onSuccess` therefore always receives a genuinely successful
|
|
* result.
|
|
*/
|
|
export const useExecuteLogsSqlMutation = ({
|
|
onSuccess,
|
|
onError,
|
|
...options
|
|
}: Omit<
|
|
UseCustomMutationOptions<ExecuteLogsSqlResult, LogsQueryError, ExecuteLogsSqlVariables>,
|
|
'mutationFn'
|
|
> = {}) => {
|
|
return useMutation<ExecuteLogsSqlResult, LogsQueryError, ExecuteLogsSqlVariables>({
|
|
async mutationFn(variables) {
|
|
let result: ExecuteLogsSqlResult
|
|
try {
|
|
result = await executeLogsSql(variables)
|
|
} catch (error) {
|
|
// Transport failure — normalize to the same shape as a query error.
|
|
throw mapLogsError(error) ?? { message: UNKNOWN_LOGS_ERROR_MESSAGE }
|
|
}
|
|
// 200-body query error — route through onError alongside transport errors.
|
|
if (result.error) throw result.error
|
|
return result
|
|
},
|
|
onSuccess,
|
|
async onError(error, variables, context) {
|
|
if (onError === undefined) {
|
|
toast.error(error.message)
|
|
} else {
|
|
onError(error, variables, context)
|
|
}
|
|
},
|
|
...options,
|
|
})
|
|
}
|