Files
supabase/apps/studio/data/privileges/table-api-access-mutation.ts
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

116 lines
3.5 KiB
TypeScript

import pgMeta from '@supabase/pg-meta'
import { joinSqlFragments, type SafeSqlFragment } from '@supabase/pg-meta/src/pg-format'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import { invalidateTablePrivilegesQuery } from './table-privileges-query'
import type { ConnectionVars } from '@/data/common.types'
import { lintKeys } from '@/data/lint/keys'
import { executeSql } from '@/data/sql/execute-sql-query'
import {
API_ACCESS_ROLES,
API_PRIVILEGE_TYPES,
type ApiPrivilegesByRole,
} from '@/lib/data-api-types'
import type { DeepReadonly } from '@/lib/type-helpers'
import type { UseCustomMutationOptions } from '@/types'
export type TableApiAccessPrivilegesVariables = ConnectionVars & {
relationId: number
privileges: DeepReadonly<ApiPrivilegesByRole>
}
export async function updateTableApiAccessPrivileges({
projectRef,
connectionString,
relationId,
privileges,
}: TableApiAccessPrivilegesVariables) {
const sqlStatements: Array<SafeSqlFragment> = []
for (const role of API_ACCESS_ROLES) {
const rolePrivileges = privileges[role]
// Determine which privileges to grant and revoke for this role
const privilegesToGrant = rolePrivileges
const privilegesToRevoke = API_PRIVILEGE_TYPES.filter((p) => !rolePrivileges.includes(p))
// Revoke privileges that should be removed
if (privilegesToRevoke.length > 0) {
const revokeGrants = privilegesToRevoke.map((privilegeType) => ({
grantee: role,
privilegeType,
relationId,
}))
const revokeSql = pgMeta.tablePrivileges.revoke(revokeGrants).sql
if (revokeSql) sqlStatements.push(revokeSql)
}
// Grant privileges that should be added
if (privilegesToGrant.length > 0) {
const grantGrants = privilegesToGrant.map((privilegeType) => ({
grantee: role,
privilegeType,
relationId,
}))
const grantSql = pgMeta.tablePrivileges.grant(grantGrants).sql
if (grantSql) sqlStatements.push(grantSql)
}
}
if (sqlStatements.length === 0) {
return null
}
const { result } = await executeSql<[]>({
projectRef,
connectionString,
sql: joinSqlFragments(sqlStatements, '\n'),
queryKey: ['table-api-access', 'update-privileges'],
})
return result
}
type UpdateTableApiAccessPrivilegesData = Awaited<ReturnType<typeof updateTableApiAccessPrivileges>>
export const useTableApiAccessPrivilegesMutation = ({
onSuccess,
onError,
...options
}: Omit<
UseCustomMutationOptions<
UpdateTableApiAccessPrivilegesData,
Error,
TableApiAccessPrivilegesVariables
>,
'mutationFn'
> = {}) => {
const queryClient = useQueryClient()
return useMutation<UpdateTableApiAccessPrivilegesData, Error, TableApiAccessPrivilegesVariables>({
mutationFn: (vars) => updateTableApiAccessPrivileges(vars),
async onSuccess(data, variables, context) {
const { projectRef } = variables
await Promise.all([
invalidateTablePrivilegesQuery(queryClient, projectRef),
// This affects the result of the RLS disabled lint, so we need to
// invalidate it
queryClient.invalidateQueries({
queryKey: lintKeys.lint(projectRef),
}),
])
await onSuccess?.(data, variables, context)
},
async onError(data, variables, context) {
if (onError === undefined) {
toast.error(`Failed to update API access privileges: ${data.message}`)
} else {
onError(data, variables, context)
}
},
...options,
})
}