import { useMemo } from 'react' import { useTablePrivilegesQuery, type TablePrivilegesData, type TablePrivilegesError, } from './table-privileges-query' import type { ConnectionVars } from '@/data/common.types' import { useIsSchemaExposed } from '@/hooks/misc/useIsSchemaExposed' import { API_ACCESS_ROLES, API_PRIVILEGE_TYPES, isApiAccessRole, isApiPrivilegeType, type ApiPrivilegesByRole, } from '@/lib/data-api-types' import type { Prettify } from '@/lib/type-helpers' import type { UseCustomQueryOptions } from '@/types' // The contents of this array are never used, so any will allow // it to be used anywhere an array of any type is required. // eslint-disable-next-line @typescript-eslint/no-explicit-any const STABLE_EMPTY_ARRAY: any[] = [] const STABLE_EMPTY_OBJECT = {} const getApiPrivilegesByRole = ( privileges: TablePrivilegesData[number]['privileges'] ): ApiPrivilegesByRole => { const privilegesByRole: ApiPrivilegesByRole = { anon: [], authenticated: [], service_role: [], } privileges.forEach((privilege) => { const { grantee, privilege_type } = privilege if (isApiAccessRole(grantee) && isApiPrivilegeType(privilege_type)) { privilegesByRole[grantee].push(privilege_type) } }) return privilegesByRole } const mapPrivilegesByTableName = ( privileges: TablePrivilegesData | undefined, schemaName: string, tableNames: Set ): Record => { if (!privileges) return {} const result: Record = {} privileges.forEach((entry) => { if (entry.schema !== schemaName) return if (!tableNames.has(entry.name)) return result[entry.name] = getApiPrivilegesByRole(entry.privileges) }) return result } export type UseTableApiAccessQueryParams = Prettify< ConnectionVars & { schemaName: string tableNames: string[] } > export type DataApiAccessType = 'none' | 'exposed-schema-no-grants' | 'access' /** * Mirrors the "granted | custom | revoked" classification used by the Data API * settings page (see getTableGrantsCTEs in packages/pg-meta privileges.ts). * - `granted`: all 3 API roles (anon/authenticated/service_role) have all 4 * CRUD privileges — the standard Data API exposure. * - `custom`: at least one grant exists but it's not the full standard set. * - `revoked`: no API role has any privilege (mapped to `exposed-schema-no-grants`). */ export type TableGrantStatus = 'granted' | 'custom' export type TableApiAccessData = | { apiAccessType: 'access' grantStatus: TableGrantStatus privileges: ApiPrivilegesByRole } | { apiAccessType: 'none' | 'exposed-schema-no-grants' } /** * Matches the "granted" branch of getTableGrantsCTEs in packages/pg-meta's * privileges.ts — all 3 API roles must have all 4 CRUD privileges. */ export const isFullyGranted = (privileges: ApiPrivilegesByRole): boolean => API_ACCESS_ROLES.every((role) => API_PRIVILEGE_TYPES.every((priv) => privileges[role].includes(priv)) ) export type TableApiAccessMap = Prettify> export type UseTableApiAccessQueryReturn = | { data: TableApiAccessMap status: 'success' isSuccess: true isPending: false isError: false } | { data: undefined status: 'pending' isSuccess: false isPending: true isError: false } | { data: undefined status: 'error' isSuccess: false isPending: false isError: true } export const useTableApiAccessQuery = ( { projectRef, connectionString, schemaName, tableNames = STABLE_EMPTY_ARRAY, }: UseTableApiAccessQueryParams, { enabled = true, ...options }: { enabled?: boolean } & Omit< UseCustomQueryOptions, 'enabled' > = {} ): UseTableApiAccessQueryReturn => { const uniqueTableNames = useMemo(() => { return new Set( tableNames.filter((tableName) => typeof tableName === 'string' && tableName.length > 0) ) }, [tableNames]) const hasTables = uniqueTableNames.size > 0 const schemaExposureStatus = useIsSchemaExposed({ projectRef, schemaName }, { enabled }) const isSchemaExposed = schemaExposureStatus.isSuccess && schemaExposureStatus.data === true const enablePrivilegesQuery = enabled && hasTables const privilegeStatus = useTablePrivilegesQuery( { projectRef, connectionString, includedSchemas: [schemaName] }, { enabled: enablePrivilegesQuery, ...options } ) const result: UseTableApiAccessQueryReturn = useMemo(() => { const isPending = !enabled || schemaExposureStatus.status === 'pending' || (enablePrivilegesQuery && privilegeStatus.isPending) if (isPending) { return { data: undefined, status: 'pending', isSuccess: false, isPending: true, isError: false, } } const isError = schemaExposureStatus.status === 'error' || (enablePrivilegesQuery && privilegeStatus.isError) if (isError) { return { data: undefined, status: 'error', isSuccess: false, isPending: false, isError: true, } } if (!hasTables) { return { data: STABLE_EMPTY_OBJECT, status: 'success', isSuccess: true, isPending: false, isError: false, } } const resultData: TableApiAccessMap = {} const tablePrivilegesByName = isSchemaExposed ? mapPrivilegesByTableName(privilegeStatus.data, schemaName, uniqueTableNames) : {} uniqueTableNames.forEach((tableName) => { if (!isSchemaExposed) { resultData[tableName] = { apiAccessType: 'none' } return } const tablePrivileges = tablePrivilegesByName[tableName] ?? { anon: [], authenticated: [], service_role: [], } const hasApiPrivileges = tablePrivileges.anon.length > 0 || tablePrivileges.authenticated.length > 0 || tablePrivileges.service_role.length > 0 resultData[tableName] = hasApiPrivileges ? { apiAccessType: 'access', grantStatus: isFullyGranted(tablePrivileges) ? 'granted' : 'custom', privileges: tablePrivileges, } : { apiAccessType: 'exposed-schema-no-grants' } }) return { data: resultData, status: 'success', isSuccess: true, isPending: false, isError: false, } }, [ enabled, enablePrivilegesQuery, hasTables, schemaExposureStatus.status, isSchemaExposed, privilegeStatus.isPending, privilegeStatus.isError, privilegeStatus.data, schemaName, uniqueTableNames, ]) return result }