From 43300d43ce653a3d4aba6e3070b9ceb700f46fa5 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Tue, 9 Jun 2026 15:49:10 +0200 Subject: [PATCH] chore: consolidate `useAPIKeysQuery` + `getKeys` into a single `useAPIKeys` hook (#46761) ## Problem - API may return a non-array shape that can crash `getKeys` because of an hard coded cast - getting API keys is cumbersome as consumers have to call two functions ## Solution - consolidate `useAPIKeysQuery` + `getKeys` into a single `useAPIKeys` hook - guard `getKeys` so that it doesn't crash if passed a non array value - update usages ## Summary by CodeRabbit * **Refactor** * Unified how project API keys are retrieved across the studio, resulting in more consistent loading/error handling and slight responsiveness improvements when showing keys and related command snippets. UI and permissions behavior remain unchanged for end users. --- .../interfaces/App/CommandMenu/ApiKeys.tsx | 8 ++--- .../interfaces/ConnectSheet/ConnectSheet.tsx | 10 +++---- .../interfaces/Database/Hooks/HTTPHeaders.tsx | 12 ++++---- .../DestinationPanelFields.tsx | 6 ++-- .../DestinationForm/index.tsx | 7 +++-- .../interfaces/Docs/Authentication.tsx | 7 +++-- .../EdgeFunctionDetails.tsx | 6 ++-- .../EdgeFunctionTesterSheet.tsx | 6 ++-- .../Functions/TerminalInstructions.tsx | 6 ++-- .../Home/NewProjectPanel/APIKeys.tsx | 8 ++--- .../CronJobs/HttpHeaderFieldsSection.tsx | 6 ++-- .../ProjectAPIDocs/Content/Introduction.tsx | 7 +++-- .../ProjectAPIDocs/ProjectAPIDocs.tsx | 6 ++-- .../ProjectHome/ProjectConnectionPopover.tsx | 6 ++-- .../Inspector/RealtimeTokensPopover.tsx | 9 +++--- .../VectorBucketTableExamplesSheet.tsx | 8 ++--- apps/studio/data/api-keys/api-keys-query.ts | 30 +++++++++++++++++++ .../iceberg-wrapper-create-mutation.ts | 7 ++--- 18 files changed, 93 insertions(+), 62 deletions(-) diff --git a/apps/studio/components/interfaces/App/CommandMenu/ApiKeys.tsx b/apps/studio/components/interfaces/App/CommandMenu/ApiKeys.tsx index d223aa14c4b..82fe7b2cbb2 100644 --- a/apps/studio/components/interfaces/App/CommandMenu/ApiKeys.tsx +++ b/apps/studio/components/interfaces/App/CommandMenu/ApiKeys.tsx @@ -15,7 +15,7 @@ import { import { COMMAND_MENU_SECTIONS } from './CommandMenu.utils' import { orderCommandSectionsByPriority } from './ordering' -import { getKeys, useAPIKeysQuery } from '@/data/api-keys/api-keys-query' +import { useAPIKeys } from '@/data/api-keys/api-keys-query' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' @@ -31,13 +31,13 @@ export function useApiKeysCommands() { const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.SECRETS_READ, '*') - const { data: apiKeys } = useAPIKeysQuery( + const { data: apiKeysData } = useAPIKeys( { projectRef: project?.ref, reveal: true }, { enabled: canReadAPIKeys } ) const commands = useMemo(() => { const { anonKey, serviceKey, publishableKey, allSecretKeys } = canReadAPIKeys - ? getKeys(apiKeys) + ? (apiKeysData ?? {}) : {} return [ @@ -127,7 +127,7 @@ export function useApiKeysCommands() { icon: () => , }, ].filter(Boolean) as ICommand[] - }, [apiKeys, canReadAPIKeys, project, ref, resetCommandMenu, setIsOpen]) + }, [canReadAPIKeys, apiKeysData, project, ref, resetCommandMenu, setIsOpen]) useRegisterPage( API_KEYS_PAGE_NAME, diff --git a/apps/studio/components/interfaces/ConnectSheet/ConnectSheet.tsx b/apps/studio/components/interfaces/ConnectSheet/ConnectSheet.tsx index 01e5a58f318..994371b3b29 100644 --- a/apps/studio/components/interfaces/ConnectSheet/ConnectSheet.tsx +++ b/apps/studio/components/interfaces/ConnectSheet/ConnectSheet.tsx @@ -12,7 +12,7 @@ import { useAvailableConnectModes } from './useAvailableConnectModes' import { useConnectSheetParams } from './useConnectSheetParams' import { useConnectSheetShortcut } from './useConnectSheetShortcut' import { useConnectState } from './useConnectState' -import { getKeys, useAPIKeysQuery } from '@/data/api-keys/api-keys-query' +import { useAPIKeys } from '@/data/api-keys/api-keys-query' import { useProjectApiUrl } from '@/data/config/project-endpoint-query' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import { useTrack } from '@/lib/telemetry/track' @@ -159,18 +159,16 @@ export const ConnectSheet = () => { PermissionAction.READ, 'service_api_keys' ) - const { data: apiKeys } = useAPIKeysQuery({ projectRef }, { enabled: canReadAPIKeys }) - const { anonKey, publishableKey } = canReadAPIKeys - ? getKeys(apiKeys) - : { anonKey: null, publishableKey: null } + const { data: apiKeysData } = useAPIKeys({ projectRef }, { enabled: canReadAPIKeys }) const projectKeys: ProjectKeys = useMemo(() => { + const { anonKey, publishableKey } = apiKeysData ?? {} return { apiUrl: endpoint, anonKey: anonKey?.api_key ?? null, publishableKey: publishableKey?.api_key ?? null, } - }, [endpoint, anonKey?.api_key, publishableKey?.api_key]) + }, [endpoint, apiKeysData]) const availableModes = useMemo( () => schema.modes.filter((m) => availableModeIds.includes(m.id)), diff --git a/apps/studio/components/interfaces/Database/Hooks/HTTPHeaders.tsx b/apps/studio/components/interfaces/Database/Hooks/HTTPHeaders.tsx index 5638114c771..b22344bbec2 100644 --- a/apps/studio/components/interfaces/Database/Hooks/HTTPHeaders.tsx +++ b/apps/studio/components/interfaces/Database/Hooks/HTTPHeaders.tsx @@ -11,7 +11,7 @@ import { FormSectionContent, FormSectionLabel, } from '@/components/ui/Forms/FormSection' -import { getKeys, useAPIKeysQuery } from '@/data/api-keys/api-keys-query' +import { useAPIKeys } from '@/data/api-keys/api-keys-query' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import { uuidv4 } from '@/lib/helpers' @@ -23,12 +23,14 @@ export const HTTPHeaders = ({ form }: HTTPHeadersProps) => { const { ref } = useParams() const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.SECRETS_READ, '*') - const { data: apiKeys } = useAPIKeysQuery( - { projectRef: ref, reveal: true }, + const { data: apiKeyData } = useAPIKeys( + { + projectRef: ref, + reveal: true, + }, { enabled: canReadAPIKeys } ) - - const { serviceKey, secretKey } = getKeys(apiKeys) + const { serviceKey, secretKey } = apiKeyData ?? {} const apiKey = secretKey?.api_key ?? serviceKey?.api_key ?? '[YOUR API KEY]' const functionType = useWatch({ control: form.control, name: 'function_type' }) diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/DestinationPanelFields.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/DestinationPanelFields.tsx index 3c4bf58612a..55e5f478d8c 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/DestinationPanelFields.tsx +++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/DestinationPanelFields.tsx @@ -24,7 +24,7 @@ import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { CREATE_NEW_KEY, CREATE_NEW_NAMESPACE } from './DestinationForm.constants' import type { DestinationPanelSchemaType } from './DestinationForm.schema' import { InlineLink } from '@/components/ui/InlineLink' -import { getKeys, useAPIKeysQuery } from '@/data/api-keys/api-keys-query' +import { useAPIKeys } from '@/data/api-keys/api-keys-query' import { useAnalyticsBucketsQuery } from '@/data/storage/analytics-buckets-query' import { useIcebergNamespacesQuery } from '@/data/storage/iceberg-namespaces-query' import { useStorageCredentialsQuery } from '@/data/storage/s3-access-key-query' @@ -370,11 +370,11 @@ export const AnalyticsBucketFields = ({ const { ref: projectRef } = useParams() const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.SECRETS_READ, '*') - const { data: apiKeys } = useAPIKeysQuery( + const { data: apiKeysData } = useAPIKeys( { projectRef, reveal: true }, { enabled: canReadAPIKeys } ) - const { serviceKey } = getKeys(apiKeys) + const { serviceKey } = apiKeysData ?? {} const serviceApiKey = serviceKey?.api_key ?? '' const { diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/index.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/index.tsx index 7d2c9d476e7..23b74ff3d7a 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/index.tsx +++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/index.tsx @@ -31,7 +31,7 @@ import { PublicationSelection } from './PublicationSelection' import { ReplicationDisclaimerDialog } from './ReplicationDisclaimerDialog' import { ValidationFailuresSection } from './ValidationFailuresSection' import { CreateAnalyticsBucketSheet } from '@/components/interfaces/Storage/AnalyticsBuckets/CreateAnalyticsBucketSheet' -import { getKeys, useAPIKeysQuery } from '@/data/api-keys/api-keys-query' +import { useAPIKeys } from '@/data/api-keys/api-keys-query' import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query' import { BatchConfig, @@ -149,11 +149,12 @@ export const DestinationForm = ({ pipelineId: existingDestination?.pipelineId, }) - const { data: apiKeys } = useAPIKeysQuery( + const { data: apiKeysData } = useAPIKeys( { projectRef, reveal: true }, { enabled: canReadAPIKeys } ) - const { serviceKey } = getKeys(apiKeys) + const { serviceKey } = apiKeysData ?? {} + const catalogToken = serviceKey?.api_key ?? '' const { data: projectSettings } = useProjectSettingsV2Query({ projectRef }) diff --git a/apps/studio/components/interfaces/Docs/Authentication.tsx b/apps/studio/components/interfaces/Docs/Authentication.tsx index d38f2440fe8..d0937e1c7e3 100644 --- a/apps/studio/components/interfaces/Docs/Authentication.tsx +++ b/apps/studio/components/interfaces/Docs/Authentication.tsx @@ -5,7 +5,7 @@ import CodeSnippet from './CodeSnippet' import { DocSection } from './DocSection' import Snippets from './Snippets' import { InlineLink } from '@/components/ui/InlineLink' -import { getKeys, useAPIKeysQuery } from '@/data/api-keys/api-keys-query' +import { useAPIKeys } from '@/data/api-keys/api-keys-query' import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' @@ -17,10 +17,11 @@ interface AuthenticationProps { const Authentication = ({ selectedLang, showApiKey }: AuthenticationProps) => { const { ref: projectRef } = useParams() const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.SECRETS_READ, '*') - const { data: apiKeys } = useAPIKeysQuery({ projectRef }, { enabled: canReadAPIKeys }) + const { data: apiKeyData } = useAPIKeys({ projectRef }, { enabled: canReadAPIKeys }) + const { anonKey, serviceKey } = apiKeyData ?? {} + const { data: settings } = useProjectSettingsV2Query({ projectRef }) - const { anonKey, serviceKey } = getKeys(apiKeys) const protocol = settings?.app_config?.protocol ?? 'https' const hostEndpoint = settings?.app_config?.endpoint const endpoint = `${protocol}://${hostEndpoint ?? ''}` diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx index a0feafae502..1d584d547fb 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx @@ -42,7 +42,7 @@ import z from 'zod' import CommandRender from '../CommandRender' import { INVOCATION_TABS } from './EdgeFunctionDetails.constants' import { generateCLICommands } from './EdgeFunctionDetails.utils' -import { getKeys, useAPIKeysQuery } from '@/data/api-keys/api-keys-query' +import { useAPIKeys } from '@/data/api-keys/api-keys-query' import { useProjectApiUrl } from '@/data/config/project-endpoint-query' import { useEdgeFunctionQuery } from '@/data/edge-functions/edge-function-query' import { useEdgeFunctionDeleteMutation } from '@/data/edge-functions/edge-functions-delete-mutation' @@ -79,7 +79,8 @@ export const EdgeFunctionDetails = () => { const canUpdateEdgeFunction = IS_PLATFORM && canUpdateEdgeFunctionPermission const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.SECRETS_READ, '*') - const { data: apiKeys } = useAPIKeysQuery({ projectRef }, { enabled: canReadAPIKeys }) + const { data: apiKeyData } = useAPIKeys({ projectRef }, { enabled: canReadAPIKeys }) + const { anonKey, publishableKey } = apiKeyData ?? {} const { data: selectedFunction } = useEdgeFunctionQuery({ projectRef, slug: functionSlug }) @@ -99,7 +100,6 @@ export const EdgeFunctionDetails = () => { defaultValues: { name: '', verify_jwt: false }, }) - const { anonKey, publishableKey } = getKeys(apiKeys) const apiKey = publishableKey?.api_key ?? anonKey?.api_key ?? '[YOUR ANON KEY]' const { managementCommands } = generateCLICommands({ diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionTesterSheet.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionTesterSheet.tsx index af30abc98aa..ac14a0b89d6 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionTesterSheet.tsx +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionTesterSheet.tsx @@ -39,7 +39,7 @@ import { HTTP_METHODS } from './EdgeFunctionDetails.constants' import { ErrorWithStatus, ResponseData } from './EdgeFunctionDetails.types' import { RoleImpersonationPopover } from '@/components/interfaces/RoleImpersonationSelector/RoleImpersonationPopover' import { ShortcutTooltip } from '@/components/ui/ShortcutTooltip' -import { getKeys, useAPIKeysQuery } from '@/data/api-keys/api-keys-query' +import { useAPIKeys } from '@/data/api-keys/api-keys-query' import { useSessionAccessTokenQuery } from '@/data/auth/session-access-token-query' import { useProjectPostgrestConfigQuery } from '@/data/config/project-postgrest-config-query' import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query' @@ -102,11 +102,11 @@ const EdgeFunctionTesterSheetContent = ({ visible, onClose }: EdgeFunctionTester const [error, setError] = useState(null) const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.SECRETS_READ, '*') - const { data: apiKeys } = useAPIKeysQuery({ projectRef }, { enabled: canReadAPIKeys }) + const { data: apiKeysData } = useAPIKeys({ projectRef }, { enabled: canReadAPIKeys }) + const { serviceKey } = apiKeysData ?? {} const { data: config } = useProjectPostgrestConfigQuery({ projectRef }) const { data: settings } = useProjectSettingsV2Query({ projectRef }) const { data: accessToken } = useSessionAccessTokenQuery({ enabled: IS_PLATFORM }) - const { serviceKey } = getKeys(apiKeys) const track = useTrack() const { mutate: testEdgeFunction, isPending } = useEdgeFunctionTestMutation({ diff --git a/apps/studio/components/interfaces/Functions/TerminalInstructions.tsx b/apps/studio/components/interfaces/Functions/TerminalInstructions.tsx index fe8e76381cc..9185e6518f4 100644 --- a/apps/studio/components/interfaces/Functions/TerminalInstructions.tsx +++ b/apps/studio/components/interfaces/Functions/TerminalInstructions.tsx @@ -9,7 +9,7 @@ import type { Commands } from './Functions.types' import CommandRender from '@/components/interfaces/Functions/CommandRender' import { DocsButton } from '@/components/ui/DocsButton' import { useAccessTokensQuery } from '@/data/access-tokens/access-tokens-query' -import { getKeys, useAPIKeysQuery } from '@/data/api-keys/api-keys-query' +import { useAPIKeys } from '@/data/api-keys/api-keys-query' import { useProjectApiUrl } from '@/data/config/project-endpoint-query' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import { DOCS_URL } from '@/lib/constants' @@ -29,12 +29,12 @@ export const TerminalInstructions = forwardRef< const { data: tokens } = useAccessTokensQuery() const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.SECRETS_READ, '*') - const { data: apiKeys } = useAPIKeysQuery({ projectRef }, { enabled: canReadAPIKeys }) + const { data: apiKeyData } = useAPIKeys({ projectRef }, { enabled: canReadAPIKeys }) + const { anonKey, publishableKey } = apiKeyData ?? {} const { data: endpoint } = useProjectApiUrl({ projectRef }) const functionsEndpoint = `${endpoint}/functions/v1` - const { anonKey, publishableKey } = getKeys(apiKeys) const apiKey = publishableKey?.api_key ?? anonKey?.api_key ?? '[YOUR ANON KEY]' // get the .co or .net TLD from the restUrl diff --git a/apps/studio/components/interfaces/Home/NewProjectPanel/APIKeys.tsx b/apps/studio/components/interfaces/Home/NewProjectPanel/APIKeys.tsx index 237dc813e83..a023a338390 100644 --- a/apps/studio/components/interfaces/Home/NewProjectPanel/APIKeys.tsx +++ b/apps/studio/components/interfaces/Home/NewProjectPanel/APIKeys.tsx @@ -11,7 +11,7 @@ import { ConnectionIcon } from '@/components/interfaces/Connect/ConnectionIcon' import { ConnectButton } from '@/components/interfaces/ConnectButton/ConnectButton' import { AlertError } from '@/components/ui/AlertError' import { InlineLink } from '@/components/ui/InlineLink' -import { getKeys, useAPIKeysQuery } from '@/data/api-keys/api-keys-query' +import { useAPIKeys } from '@/data/api-keys/api-keys-query' import { useLegacyAPIKeysStatusQuery } from '@/data/api-keys/legacy-api-keys-status-query' import { useJwtSecretUpdatingStatusQuery } from '@/data/config/jwt-secret-updating-status-query' import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query' @@ -32,12 +32,12 @@ export const APIKeys = () => { useLegacyAPIKeysStatusQuery({ projectRef }, { enabled: canReadAPIKeys }) const { - data: apiKeys, + data: apiKeysData, error: errorAPIKeys, isError: isErrorAPIKeys, isPending: isLoadingAPIKeys, - } = useAPIKeysQuery({ projectRef }, { enabled: canReadAPIKeys }) - const { anonKey, serviceKey, publishableKey, secretKey } = getKeys(apiKeys) + } = useAPIKeys({ projectRef }, { enabled: canReadAPIKeys }) + const { anonKey, serviceKey, publishableKey, secretKey } = apiKeysData ?? {} const hasNewAPIKeys = !!publishableKey && !!secretKey const isLegacyKeysEnabled = legacyAPIKeysStatusData?.enabled ?? false diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/HttpHeaderFieldsSection.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/HttpHeaderFieldsSection.tsx index c8ca4054481..5212f876416 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/HttpHeaderFieldsSection.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/HttpHeaderFieldsSection.tsx @@ -6,7 +6,7 @@ import { KeyValueFieldArray } from 'ui-patterns/form/KeyValueFieldArray/KeyValue import { CreateCronJobForm } from './CreateCronJobSheet/CreateCronJobSheet.constants' import { buildEdgeFunctionHeaderAddActions } from '@/components/interfaces/Functions/httpHeaderAddActions' -import { getKeys, useAPIKeysQuery } from '@/data/api-keys/api-keys-query' +import { useAPIKeys } from '@/data/api-keys/api-keys-query' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' interface HTTPHeaderFieldsSectionProps { @@ -18,12 +18,12 @@ export const HTTPHeaderFieldsSection = ({ variant }: HTTPHeaderFieldsSectionProp const { ref } = useParams() const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.SECRETS_READ, '*') - const { data: apiKeys } = useAPIKeysQuery( + const { data: apiKeysData } = useAPIKeys( { projectRef: ref, reveal: true }, { enabled: canReadAPIKeys } ) + const { serviceKey, secretKey } = apiKeysData ?? {} - const { serviceKey, secretKey } = getKeys(apiKeys) const apiKey = secretKey?.api_key ?? serviceKey?.api_key ?? '[YOUR API KEY]' const addActions = variant === 'edge_function' diff --git a/apps/studio/components/interfaces/ProjectAPIDocs/Content/Introduction.tsx b/apps/studio/components/interfaces/ProjectAPIDocs/Content/Introduction.tsx index 4594cee9971..a29d0fdfe3e 100644 --- a/apps/studio/components/interfaces/ProjectAPIDocs/Content/Introduction.tsx +++ b/apps/studio/components/interfaces/ProjectAPIDocs/Content/Introduction.tsx @@ -9,7 +9,7 @@ import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import ContentSnippet from '../ContentSnippet' import { DOCS_CONTENT } from '../ProjectAPIDocs.constants' import type { ContentProps } from './Content.types' -import { getKeys, useAPIKeysQuery } from '@/data/api-keys/api-keys-query' +import { useAPIKeys } from '@/data/api-keys/api-keys-query' import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import { useTrack } from '@/lib/telemetry/track' @@ -17,8 +17,10 @@ import { useTrack } from '@/lib/telemetry/track' export const Introduction = ({ showKeys, language, apikey, endpoint }: ContentProps) => { const { ref } = useParams() const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.SECRETS_READ, '*') - const { data: apiKeys } = useAPIKeysQuery({ projectRef: ref }, { enabled: canReadAPIKeys }) + const { data: apiKeysData } = useAPIKeys({ projectRef: ref }, { enabled: canReadAPIKeys }) useProjectSettingsV2Query({ projectRef: ref }) + const { anonKey, serviceKey } = apiKeysData ?? {} + const track = useTrack() const [copied, setCopied] = useState<'anon' | 'service'>() @@ -27,7 +29,6 @@ export const Introduction = ({ showKeys, language, apikey, endpoint }: ContentPr if (copied !== undefined) setTimeout(() => setCopied(undefined), 2000) }, [copied]) - const { anonKey, serviceKey } = getKeys(apiKeys) const anonApiKey = anonKey?.api_key const serviceApiKey = serviceKey?.api_key ?? 'SUPABASE_CLIENT_SERVICE_KEY' diff --git a/apps/studio/components/interfaces/ProjectAPIDocs/ProjectAPIDocs.tsx b/apps/studio/components/interfaces/ProjectAPIDocs/ProjectAPIDocs.tsx index 2e86d2b6937..b5236ff29f4 100644 --- a/apps/studio/components/interfaces/ProjectAPIDocs/ProjectAPIDocs.tsx +++ b/apps/studio/components/interfaces/ProjectAPIDocs/ProjectAPIDocs.tsx @@ -17,7 +17,7 @@ import { UserManagement } from './Content/UserManagement' import { FirstLevelNav } from './FirstLevelNav' import LanguageSelector from './LanguageSelector' import { SecondLevelNav } from './SecondLevelNav' -import { getKeys, useAPIKeysQuery } from '@/data/api-keys/api-keys-query' +import { useAPIKeys } from '@/data/api-keys/api-keys-query' import { useProjectApiUrl } from '@/data/config/project-endpoint-query' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import { useAppStateSnapshot } from '@/state/app-state' @@ -45,17 +45,17 @@ export const ProjectAPIDocs = () => { const language = snap.docsLanguage const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.SECRETS_READ, '*') - const { data: apiKeys } = useAPIKeysQuery( + const { data: apiKeysData } = useAPIKeys( { projectRef: ref }, { enabled: snap.showProjectApiDocs && canReadAPIKeys } ) + const { anonKey } = apiKeysData ?? {} const { data: endpoint } = useProjectApiUrl( { projectRef: ref }, { enabled: snap.showProjectApiDocs } ) - const { anonKey } = getKeys(apiKeys) const apikey = showKeys ? (anonKey?.api_key ?? 'SUPABASE_CLIENT_ANON_KEY') : 'SUPABASE_CLIENT_ANON_KEY' diff --git a/apps/studio/components/interfaces/ProjectHome/ProjectConnectionPopover.tsx b/apps/studio/components/interfaces/ProjectHome/ProjectConnectionPopover.tsx index b126619052b..c9911399469 100644 --- a/apps/studio/components/interfaces/ProjectHome/ProjectConnectionPopover.tsx +++ b/apps/studio/components/interfaces/ProjectHome/ProjectConnectionPopover.tsx @@ -15,7 +15,7 @@ import { import { ShimmeringLoader } from 'ui-patterns' import { getConnectionStrings } from '@/components/interfaces/Connect/DatabaseSettings.utils' -import { getKeys, useAPIKeysQuery } from '@/data/api-keys/api-keys-query' +import { useAPIKeys } from '@/data/api-keys/api-keys-query' import { useProjectApiUrl } from '@/data/config/project-endpoint-query' import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' @@ -47,11 +47,11 @@ export const ProjectConnectionPopover = ({ projectRef }: ProjectConnectionPopove const { data: projectUrl, isPending: isLoadingApiUrl } = useProjectApiUrl({ projectRef }) - const { data: apiKeys, isLoading: isLoadingKeys } = useAPIKeysQuery( + const { data, isLoading: isLoadingKeys } = useAPIKeys( { projectRef }, { enabled: open && canReadAPIKeys } ) - const { publishableKey } = canReadAPIKeys ? getKeys(apiKeys) : { publishableKey: null } + const { publishableKey } = data ?? {} const { data: databases, isLoading: isLoadingDatabases } = useReadReplicasQuery( { projectRef }, diff --git a/apps/studio/components/interfaces/Realtime/Inspector/RealtimeTokensPopover.tsx b/apps/studio/components/interfaces/Realtime/Inspector/RealtimeTokensPopover.tsx index e0b304f71f6..537edafb374 100644 --- a/apps/studio/components/interfaces/Realtime/Inspector/RealtimeTokensPopover.tsx +++ b/apps/studio/components/interfaces/Realtime/Inspector/RealtimeTokensPopover.tsx @@ -4,7 +4,7 @@ import { toast } from 'sonner' import { RealtimeConfig } from './useRealtimeMessages' import { RoleImpersonationPopover } from '@/components/interfaces/RoleImpersonationSelector/RoleImpersonationPopover' -import { getKeys, useAPIKeysQuery } from '@/data/api-keys/api-keys-query' +import { useAPIKeys } from '@/data/api-keys/api-keys-query' import { getTemporaryAPIKey } from '@/data/api-keys/temp-api-keys-query' import { useProjectPostgrestConfigQuery } from '@/data/config/project-postgrest-config-query' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' @@ -22,15 +22,13 @@ export const RealtimeTokensPopover = ({ config, onChangeConfig }: RealtimeTokens const snap = useRoleImpersonationStateSnapshot() const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.SECRETS_READ, '*') - const { data: apiKeys } = useAPIKeysQuery( + const { data: apiKeysData } = useAPIKeys( { projectRef: config.projectRef, reveal: true, }, { enabled: canReadAPIKeys } ) - const { anonKey, publishableKey } = getKeys(apiKeys) - const { data: postgrestConfig } = useProjectPostgrestConfigQuery( { projectRef: config.projectRef }, { enabled: IS_PLATFORM } @@ -55,6 +53,7 @@ export const RealtimeTokensPopover = ({ config, onChangeConfig }: RealtimeTokens }, [snap.role]) useEffect(() => { + const { anonKey, publishableKey } = apiKeysData ?? {} const triggerUpdateTokenBearer = async () => { let token: string | undefined let bearer: string | null = null @@ -84,7 +83,7 @@ export const RealtimeTokensPopover = ({ config, onChangeConfig }: RealtimeTokens triggerUpdateTokenBearer() // eslint-disable-next-line react-hooks/exhaustive-deps - }, [snap.role, anonKey]) + }, [snap.role, apiKeysData]) return } diff --git a/apps/studio/components/interfaces/Storage/VectorBuckets/VectorBucketDetails/VectorBucketTableExamplesSheet.tsx b/apps/studio/components/interfaces/Storage/VectorBuckets/VectorBucketDetails/VectorBucketTableExamplesSheet.tsx index 7a96ed77d3b..50189738df3 100644 --- a/apps/studio/components/interfaces/Storage/VectorBuckets/VectorBucketDetails/VectorBucketTableExamplesSheet.tsx +++ b/apps/studio/components/interfaces/Storage/VectorBuckets/VectorBucketDetails/VectorBucketTableExamplesSheet.tsx @@ -28,7 +28,7 @@ import { CodeBlock } from 'ui-patterns/CodeBlock' import { useS3VectorsWrapperExtension } from '../useS3VectorsWrapper' import { useS3VectorsWrapperInstance } from '../useS3VectorsWrapperInstance' import { DocsButton } from '@/components/ui/DocsButton' -import { getKeys, useAPIKeysQuery } from '@/data/api-keys/api-keys-query' +import { useAPIKeys } from '@/data/api-keys/api-keys-query' import { VectorBucketIndex } from '@/data/storage/vector-buckets-indexes-query' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import { DOCS_URL } from '@/lib/constants' @@ -143,8 +143,8 @@ function VectorBucketIndexExamples({ PermissionAction.READ, 'service_api_keys' ) - const { data: apiKeys } = useAPIKeysQuery({ projectRef }, { enabled: canReadAPIKeys }) - const { secretKey } = canReadAPIKeys ? getKeys(apiKeys) : { secretKey: null } + const { data: apiKeysData } = useAPIKeys({ projectRef }, { enabled: canReadAPIKeys }) + const { secretKey } = apiKeysData ?? {} const { data: wrapperInstance } = useS3VectorsWrapperInstance({ bucketId }) const foreignTable = wrapperInstance?.tables?.find((x) => x.name === indexName) @@ -177,7 +177,7 @@ values const jsCode = `import { createClient } from '@supabase/supabase-js' -// Adding vector data requires a secret or service role key +// Adding vector data requires a secret or service role key // This code SHOULD NOT be run on the client side as you will be vulnerable to a data leak const client = createClient( process.env.SUPABASE_URL, diff --git a/apps/studio/data/api-keys/api-keys-query.ts b/apps/studio/data/api-keys/api-keys-query.ts index be0f873b947..a9f87625e38 100644 --- a/apps/studio/data/api-keys/api-keys-query.ts +++ b/apps/studio/data/api-keys/api-keys-query.ts @@ -1,4 +1,5 @@ import { useQuery } from '@tanstack/react-query' +import { useCallback } from 'react' import { apiKeysKeys } from './keys' import { get, handleError } from '@/data/fetchers' @@ -78,7 +79,18 @@ export const useAPIKeysQuery = ( }) } +const EmptyKey = { + anonKey: undefined, + serviceKey: undefined, + publishableKey: undefined, + secretKey: undefined, + allSecretKeys: [], +} + export const getKeys = (apiKeys: APIKey[] = []) => { + if (!Array.isArray(apiKeys)) { + return EmptyKey + } const anonKey = apiKeys.find((x) => x.name === 'anon') const serviceKey = apiKeys.find((x) => x.name === 'service_role') @@ -90,3 +102,21 @@ export const getKeys = (apiKeys: APIKey[] = []) => { return { anonKey, serviceKey, publishableKey, secretKey, allSecretKeys } } + +const PlaceholderData: APIKeysData = [] + +export const useAPIKeys = ( + variables: APIKeysVariables, + options: UseCustomQueryOptions = {} +) => { + const select = useCallback( + (data: APIKeysData) => getKeys(options.enabled ? data : []), + [options.enabled] + ) + + return useAPIKeysQuery(variables, { + ...options, + placeholderData: PlaceholderData, + select, + }) +} diff --git a/apps/studio/data/storage/iceberg-wrapper-create-mutation.ts b/apps/studio/data/storage/iceberg-wrapper-create-mutation.ts index 3876f34ff2a..c98ad6d0217 100644 --- a/apps/studio/data/storage/iceberg-wrapper-create-mutation.ts +++ b/apps/studio/data/storage/iceberg-wrapper-create-mutation.ts @@ -10,7 +10,7 @@ import { getCatalogURI, getConnectionURL, } from '@/components/interfaces/Storage/StorageSettings/StorageSettings.utils' -import { getKeys, useAPIKeysQuery } from '@/data/api-keys/api-keys-query' +import { useAPIKeys } from '@/data/api-keys/api-keys-query' import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query' import { FDWCreateVariables, useFDWCreateMutation } from '@/data/fdw/fdw-create-mutation' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' @@ -20,12 +20,11 @@ export const useIcebergWrapperCreateMutation = () => { const { data: project } = useSelectedProjectQuery() const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.SECRETS_READ, '*') - const { data: apiKeys } = useAPIKeysQuery( + const { data: apiKeysData } = useAPIKeys( { projectRef: project?.ref, reveal: true }, { enabled: canReadAPIKeys } ) - const { secretKey, serviceKey } = getKeys(apiKeys) - + const { secretKey, serviceKey } = apiKeysData ?? {} const { data: settings } = useProjectSettingsV2Query({ projectRef: project?.ref }) const protocol = settings?.app_config?.protocol ?? 'https' const endpoint = settings?.app_config?.storage_endpoint || settings?.app_config?.endpoint