From ef247b8f5ff580836eb07e673bd56a39272310c9 Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Fri, 28 Mar 2025 13:17:29 -0400 Subject: [PATCH] feat(docs): fetch conn string (#34492) * feat(docs): fetch conn string Add option to ProjectConfigVariables to fetch the Supavisor session mode connection string, for setting up MCP server * docs(mcp): reminder to replace password placeholder in connection string * fix: display helpful message if project paused --------- Co-authored-by: Greg Richardson --- .../ProjectConfigVariables.tsx | 63 ++++++++++++++----- .../ProjectConfigVariables.utils.ts | 3 +- .../content/guides/getting-started/mcp.mdx | 10 ++- apps/docs/lib/fetch/pooler.ts | 42 +++++++++++++ apps/docs/lib/fetch/projects.ts | 7 +++ apps/docs/types/utils.ts | 3 + 6 files changed, 110 insertions(+), 18 deletions(-) create mode 100644 apps/docs/lib/fetch/pooler.ts create mode 100644 apps/docs/types/utils.ts diff --git a/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.tsx b/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.tsx index c85641de578..41250bcb1dc 100644 --- a/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.tsx +++ b/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.tsx @@ -33,8 +33,9 @@ import { import { useCopy } from '~/hooks/useCopy' import { useBranchesQuery } from '~/lib/fetch/branches' import { useOrganizationsQuery } from '~/lib/fetch/organizations' +import { type SupavisorConfigData, useSupavisorConfigQuery } from '~/lib/fetch/pooler' import { useProjectApiQuery } from '~/lib/fetch/projectApi' -import { useProjectsQuery } from '~/lib/fetch/projects' +import { isProjectPaused, type ProjectsData, useProjectsQuery } from '~/lib/fetch/projects' import { LOCAL_STORAGE_KEYS, retrieve, storeOrRemoveNull } from '~/lib/storage' import { useOnLogout } from '~/lib/userAuth' @@ -59,6 +60,7 @@ type VariableDataState = | 'userLoading' | 'loggedOut' | 'loggedIn.noSelectedProject' + | 'loggedIn.selectedProject.projectPaused' | 'loggedIn.selectedProject.dataPending' | 'loggedIn.selectedProject.dataError' | 'loggedIn.selectedProject.dataSuccess' @@ -187,18 +189,19 @@ function BranchSelector() { const { selectedProject, selectedBranch, setSelectedBranch } = useSnapshot(projectsStore) + const projectPaused = isProjectPaused(selectedProject) const hasBranches = selectedProject?.is_branch_enabled ?? false const { data, isPending, isError } = useBranchesQuery( { projectRef: selectedProject?.ref }, - { enabled: isLoggedIn && hasBranches } + { enabled: isLoggedIn && !projectPaused && hasBranches } ) const stateSummary: BranchesDataState = userLoading ? 'userLoading' : !isLoggedIn ? 'loggedOut' - : !hasBranches + : !hasBranches || projectPaused ? 'loggedIn.noBranches' : isPending ? 'loggedIn.branches.dataPending' @@ -265,22 +268,36 @@ function VariableView({ variable, className }: { variable: Variable; className?: const isLoggedIn = useIsLoggedIn() const { selectedProject, selectedBranch } = useSnapshot(projectsStore) - + const projectPaused = isProjectPaused(selectedProject) const hasBranches = selectedProject?.is_branch_enabled ?? false const ref = hasBranches ? selectedBranch?.project_ref : selectedProject?.ref + const needsApiQuery = variable === 'anonKey' || variable === 'url' + const needsSupavisorQuery = variable === 'sessionPooler' + const { data: apiData, - isPending, - isError, + isPending: isApiPending, + isError: isApiError, } = useProjectApiQuery( { projectRef: ref, }, - { enabled: isLoggedIn && !!ref } + { enabled: isLoggedIn && !!ref && !projectPaused && needsApiQuery } ) - function isInvalid(apiData: ProjectApiData) { + const { + data: supavisorConfig, + isPending: isSupavisorPending, + isError: isSupavisorError, + } = useSupavisorConfigQuery( + { + projectRef: ref, + }, + { enabled: isLoggedIn && !!ref && !projectPaused && needsSupavisorQuery } + ) + + function isInvalidApiData(apiData: ProjectApiData) { switch (variable) { case 'url': return !apiData.app_config?.endpoint @@ -289,17 +306,27 @@ function VariableView({ variable, className }: { variable: Variable; className?: } } + function isInvalidSupavisorData(supavisorData: SupavisorConfigData) { + return supavisorData.length === 0 + } + const stateSummary: VariableDataState = isUserLoading ? 'userLoading' : !isLoggedIn ? 'loggedOut' : !ref ? 'loggedIn.noSelectedProject' - : isPending - ? 'loggedIn.selectedProject.dataPending' - : isError || isInvalid(apiData) - ? 'loggedIn.selectedProject.dataError' - : 'loggedIn.selectedProject.dataSuccess' + : projectPaused + ? 'loggedIn.selectedProject.projectPaused' + : (needsApiQuery ? isApiPending : isSupavisorPending) + ? 'loggedIn.selectedProject.dataPending' + : ( + needsApiQuery + ? isApiError || isInvalidApiData(apiData) + : isSupavisorError || isInvalidSupavisorData(supavisorConfig) + ) + ? 'loggedIn.selectedProject.dataError' + : 'loggedIn.selectedProject.dataSuccess' let variableValue: string = null if (stateSummary === 'loggedIn.selectedProject.dataSuccess') { @@ -310,6 +337,8 @@ function VariableView({ variable, className }: { variable: Variable; className?: case 'anonKey': variableValue = apiData.service_api_keys!.find((key) => key.tags === 'anon')!.api_key break + case 'sessionPooler': + variableValue = supavisorConfig[0].connection_string } } @@ -326,9 +355,11 @@ function VariableView({ variable, className }: { variable: Variable; className?: stateSummary === 'userLoading' || stateSummary === 'loggedIn.selectedProject.dataPending' ? 'Loading...' - : stateSummary === 'loggedIn.selectedProject.dataSuccess' - ? variableValue - : `YOUR ${prettyFormatVariable[variable].toUpperCase()}` + : stateSummary === 'loggedIn.selectedProject.projectPaused' + ? 'PROJECT PAUSED' + : stateSummary === 'loggedIn.selectedProject.dataSuccess' + ? variableValue + : `YOUR ${prettyFormatVariable[variable].toUpperCase()}` } /> diff --git a/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.utils.ts b/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.utils.ts index 08ed725f754..1d1a125e3dd 100644 --- a/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.utils.ts +++ b/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.utils.ts @@ -6,7 +6,7 @@ export type Org = OrganizationsData[number] export type Project = ProjectsData[number] export type Branch = BranchesData[number] -export type Variable = 'url' | 'anonKey' +export type Variable = 'url' | 'anonKey' | 'sessionPooler' function removeDoubleQuotes(str: string) { return str.replaceAll('"', '') @@ -31,6 +31,7 @@ function unescapeDoubleQuotes(str: string) { export const prettyFormatVariable: Record = { url: 'Project URL', anonKey: 'Anon key', + sessionPooler: 'Connection string (pooler session mode)', } type DeepReadonly = { diff --git a/apps/docs/content/guides/getting-started/mcp.mdx b/apps/docs/content/guides/getting-started/mcp.mdx index 9d9987297ac..e6f8ade1e38 100644 --- a/apps/docs/content/guides/getting-started/mcp.mdx +++ b/apps/docs/content/guides/getting-started/mcp.mdx @@ -27,11 +27,19 @@ To get started, you will need to retrieve your database connection string. These #### For a hosted Supabase instance -When running a hosted instance of Supabase, you can find your connection string by: + + +Log in and choose a project above to find your connection string, or find it in the Dashboard: 1. Navigating to your project's [Connection settings](/dashboard/project/_/settings/database?showConnect=true) 1. Copying the connection string found under **Session pooler**. + + +Be sure to replace `[YOUR-PASSWORD]` with your database password. + + + #### For a local Supabase instance When running a local instance of Supabase via the [CLI](/docs/reference/cli/introduction), you can find your connection string by running: diff --git a/apps/docs/lib/fetch/pooler.ts b/apps/docs/lib/fetch/pooler.ts new file mode 100644 index 00000000000..2d708ca6442 --- /dev/null +++ b/apps/docs/lib/fetch/pooler.ts @@ -0,0 +1,42 @@ +import { type UseQueryOptions, useQuery } from '@tanstack/react-query' + +import type { ResponseError } from '~/types/fetch' +import { get } from './fetchWrappers' + +const poolerKeys = { + supavisorConfig: (projectRef: string | undefined) => ['supavisor', 'config', projectRef] as const, +} + +export interface SupavisorConfigVariables { + projectRef?: string +} + +async function getSupavisorConfig({ projectRef }: SupavisorConfigVariables, signal?: AbortSignal) { + if (!projectRef) throw Error('projectRef is required') + + const { data, error } = await get(`/platform/projects/{ref}/config/supavisor`, { + params: { path: { ref: projectRef } }, + signal, + }) + if (error) throw error + + return data +} + +export type SupavisorConfigData = Awaited> +type SupavisorConfigError = ResponseError + +export function useSupavisorConfigQuery( + { projectRef }: SupavisorConfigVariables, + { + enabled = true, + ...options + }: Omit, 'queryKey'> +) { + return useQuery({ + queryKey: poolerKeys.supavisorConfig(projectRef), + queryFn: ({ signal }) => getSupavisorConfig({ projectRef }, signal), + enabled, + ...options, + }) +} diff --git a/apps/docs/lib/fetch/projects.ts b/apps/docs/lib/fetch/projects.ts index a4ca1a5872d..de4771ee14f 100644 --- a/apps/docs/lib/fetch/projects.ts +++ b/apps/docs/lib/fetch/projects.ts @@ -1,6 +1,7 @@ import { useQuery, UseQueryOptions } from '@tanstack/react-query' import type { ResponseError } from '~/types/fetch' import { get } from './fetchWrappers' +import { type ReadonlyRecursive } from '~/types/utils' const projectKeys = { list: () => ['all-projects'] as const, @@ -26,3 +27,9 @@ export function useProjectsQuery({ ...options, }) } + +export function isProjectPaused( + project: ReadonlyRecursive | null +): boolean | undefined { + return !project ? undefined : project.status === 'INACTIVE' +} diff --git a/apps/docs/types/utils.ts b/apps/docs/types/utils.ts new file mode 100644 index 00000000000..909ccea7bff --- /dev/null +++ b/apps/docs/types/utils.ts @@ -0,0 +1,3 @@ +export type ReadonlyRecursive = { + readonly [P in keyof T]: ReadonlyRecursive +}