Files
supabase/apps/studio/data/reports/api-report-query.ts
T
Charis 2d4e87f579 studio: SafeSql for reports, query performance, privileges (4/7) (#45998)
## Summary

Part 4 of the SafeSql migration stack
([#45897](https://github.com/supabase/supabase/pull/45897),
[#45903](https://github.com/supabase/supabase/pull/45903),
[#45990](https://github.com/supabase/supabase/pull/45990), this PR, …).

Converts the remaining reports, query performance, observability, index
advisor, and privileges call sites of `executeSql` to produce
`SafeSqlFragment` values. The `ReportQuery.sql` field flips from
`string` to `SafeSqlFragment`, which cascades into every consumer —
landed here atomically so each branch typechecks cleanly.

Touched areas:

- `interfaces/Reports/*` — `ReportQuery.sql: SafeSqlFragment`, plus all
report definitions/utilities updated
- `interfaces/QueryPerformance/useQueryPerformanceQuery.ts`
- `interfaces/Database/IndexAdvisor/*` and
`data/database/{table-index-advisor,retrieve-index-advisor-result}-query.ts`
-
`data/privileges/{table-api-access,update-exposed-entities}-mutation.ts`
- `interfaces/Storage/StoragePolicies/StoragePolicies.tsx`
- `hooks/analytics/useDbQuery.tsx`
- `Observability/useSlowQueriesCount.ts` +
`useQueryInsightsIssues.utils.test.ts`

## Test plan

- [x] `pnpm typecheck` passes
- [x] `useQueryInsightsIssues.utils.test.ts` passes
- [x] Dev-server smoke test: reports pages, query performance, index
advisor, storage policies

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Refactor**
* Reworked SQL construction and typings across reporting, query
performance, index advisor, and privilege features to use safer SQL
fragments, improving reliability and preventing query composition
issues.
* **Types**
* Reporting query types were split to distinguish database vs. logs
queries, enabling correct handling and validation.
* **Docs/Utils**
  * Added a helper to consistently generate logs SQL for report hooks.
* **Tests**
  * Updated tests to exercise the new SQL-building API.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/45998)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-15 14:50:38 -04:00

168 lines
5.4 KiB
TypeScript

import { useParams } from 'common'
import { isEqual } from 'lodash'
import { useEffect, useState } from 'react'
import { PRESET_CONFIG } from '@/components/interfaces/Reports/Reports.constants'
import { ReportFilterItem } from '@/components/interfaces/Reports/Reports.types'
import { getLogsSql, queriesFactory } from '@/components/interfaces/Reports/Reports.utils'
import type { LogsEndpointParams } from '@/components/interfaces/Settings/Logs/Logs.types'
import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector'
export const useApiReport = () => {
const { ref: projectRef } = useParams()
const state = useDatabaseSelectorStateSnapshot()
const identifier = state.selectedDatabaseId
const [filters, setFilters] = useState<ReportFilterItem[]>([])
const queryHooks = queriesFactory<keyof typeof PRESET_CONFIG.api.queries>(
PRESET_CONFIG.api.queries,
projectRef ?? 'default'
)
const totalRequests = queryHooks.totalRequests()
const topRoutes = queryHooks.topRoutes()
const errorCounts = queryHooks.errorCounts()
const topErrorRoutes = queryHooks.topErrorRoutes()
const responseSpeed = queryHooks.responseSpeed()
const topSlowRoutes = queryHooks.topSlowRoutes()
const networkTraffic = queryHooks.networkTraffic()
const requestsByCountry = queryHooks.requestsByCountry()
const activeHooks = [
totalRequests,
topRoutes,
errorCounts,
topErrorRoutes,
responseSpeed,
topSlowRoutes,
networkTraffic,
requestsByCountry,
]
const addFilter = (filter: ReportFilterItem) => {
// use a deep equal when comparing objects.
if (filters.some((f) => isEqual(f, filter))) return
setFilters((prev) =>
[...prev, filter].sort((a, b) => {
const keyA = a.key.toLowerCase()
const keyB = b.key.toLowerCase()
if (keyA < keyB) {
return -1
}
if (keyA > keyB) {
return 1
}
return 0
})
)
}
const removeFilter = (filter: ReportFilterItem) => removeFilters([filter])
const removeFilters = (toRemove: ReportFilterItem[]) => {
setFilters((prev) => {
return prev.filter((f) => !toRemove.find((r) => isEqual(f, r)))
})
}
// [Joshen] Keeping database selector separate from filter state, and merging them here for simplicity
const formattedFilters: ReportFilterItem[] = [
...filters,
...(identifier !== undefined
? [{ key: 'identifier', value: `'${identifier}'`, compare: 'is' } as ReportFilterItem]
: []),
]
useEffect(() => {
// update sql for each query
if (totalRequests.changeQuery) {
totalRequests.changeQuery(
getLogsSql(PRESET_CONFIG.api.queries.totalRequests, formattedFilters)
)
}
if (topRoutes.changeQuery) {
topRoutes.changeQuery(getLogsSql(PRESET_CONFIG.api.queries.topRoutes, formattedFilters))
}
if (errorCounts.changeQuery) {
errorCounts.changeQuery(getLogsSql(PRESET_CONFIG.api.queries.errorCounts, formattedFilters))
}
if (topErrorRoutes.changeQuery) {
topErrorRoutes.changeQuery(
getLogsSql(PRESET_CONFIG.api.queries.topErrorRoutes, formattedFilters)
)
}
if (responseSpeed.changeQuery) {
responseSpeed.changeQuery(
getLogsSql(PRESET_CONFIG.api.queries.responseSpeed, formattedFilters)
)
}
if (topSlowRoutes.changeQuery) {
topSlowRoutes.changeQuery(
getLogsSql(PRESET_CONFIG.api.queries.topSlowRoutes, formattedFilters)
)
}
if (networkTraffic.changeQuery) {
networkTraffic.changeQuery(
getLogsSql(PRESET_CONFIG.api.queries.networkTraffic, formattedFilters)
)
}
if (requestsByCountry.changeQuery) {
requestsByCountry.changeQuery(
getLogsSql(PRESET_CONFIG.api.queries.requestsByCountry, formattedFilters)
)
}
}, [JSON.stringify(formattedFilters)])
const handleRefresh = async () => {
activeHooks.forEach((hook) => hook.runQuery())
}
const handleSetParams = (params: Partial<LogsEndpointParams>) => {
activeHooks.forEach((hook) => {
hook.setParams?.((prev: LogsEndpointParams) => ({ ...prev, ...params }))
})
}
const isLoading = activeHooks.some((hook) => hook.isLoading)
return {
data: {
totalRequests: totalRequests.logData,
errorCounts: errorCounts.logData,
responseSpeed: responseSpeed.logData,
topRoutes: topRoutes.logData,
topErrorRoutes: topErrorRoutes.logData,
topSlowRoutes: topSlowRoutes.logData,
networkTraffic: networkTraffic.logData,
requestsByCountry: requestsByCountry.logData,
},
params: {
totalRequests: totalRequests.params,
errorCounts: errorCounts.params,
responseSpeed: responseSpeed.params,
topRoutes: topRoutes.params,
topErrorRoutes: topErrorRoutes.params,
topSlowRoutes: topSlowRoutes.params,
networkTraffic: networkTraffic.params,
requestsByCountry: requestsByCountry.params,
},
error: {
totalRequest: totalRequests.error,
errorCounts: errorCounts.error,
responseSpeed: responseSpeed.error,
topRoutes: topRoutes.error,
topErrorRoute: topErrorRoutes.error,
topSlowRoutes: topSlowRoutes.error,
networkTraffic: networkTraffic.error,
requestsByCountry: requestsByCountry.error,
},
mergeParams: handleSetParams,
filters,
addFilter,
removeFilter,
removeFilters,
isLoading,
refresh: handleRefresh,
}
}