Files
supabase/apps/studio/data/projects/project-detail-query.ts
Gildas GarciaandAlaister Young 737b8595f2 Update API types (#50234)
## Problem

platform, v1 and v2 have been already completely migrated and introduced
some changes.

Some types have been renamed, some outputs and inputs updated.

## Solution

- Update the API types
- Fix the TS errors

## Update

Taking this over to unblock #50134, which needs the new scoped token
permission ids from the regenerated types.

- Merged `master`.
- Regenerated `api-v2.d.ts` from the production spec. The previous files
came from a local API that exposed a webhook events endpoint production
doesn't have yet. Production has since added standardized 400 error
responses on the v2 organization endpoints. `api-v1.d.ts` and
`platform.d.ts` already matched production.
- Fixed `verify-production-types`. It formatted the regenerated files in
a temp directory outside the repository, so Prettier fell back to its
defaults and the comparison could never match the committed files. It
now passes the repository config explicitly. `pnpm api:verify-types`
passes on this branch.
- Verified locally: `pnpm typecheck`, `pnpm api:verify-types`, Studio
unit tests.

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

* **Bug Fixes**
* Preserved descriptions when saving, sharing, moving, or unsharing
notebooks, reports, SQL snippets, and saved queries.
* Improved handling of empty or null values across notebook
descriptions, billing usage, pooler settings, and infrastructure fields.
* Improved read-replica connection handling, including read-only
connection strings.
* Updated storage configuration and capability handling to match current
settings.

* **API and Compatibility**
* Updated organization, project, storage, OAuth, billing, and
infrastructure data handling to match current API responses.
  * OAuth app creation and updates now require scopes.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com>
2026-09-11 12:17:49 +08:00

229 lines
7.0 KiB
TypeScript

import { QueryClient, useQuery, useQueryClient } from '@tanstack/react-query'
import { useCallback } from 'react'
import { replicaKeys } from '../read-replicas/keys'
import { ReadReplicasData } from '../read-replicas/replicas-query'
import { projectKeys } from './keys'
import { OrgProjectsResponse } from './org-projects-infinite-query'
import { getProjectStatusOverride } from './project-status-override'
import type { components } from '@/data/api'
import { get, handleError, isValidConnString, post } from '@/data/fetchers'
import type { ResponseError, UseCustomQueryOptions } from '@/types'
type ProjectDetailVariables = { ref?: string; skipWake?: boolean }
export type ProjectDetail = components['schemas']['ProjectDetailResponse_Output']
export interface Project extends Omit<ProjectDetail, 'status'> {
/**
* postgrestStatus is available on client side only.
* We use this status to check if a project instance is HEALTHY or not
* If not we will show ConnectingState and run a polling until it's back online
*/
postgrestStatus?: 'ONLINE' | 'OFFLINE'
status: ProjectDetail['status']
}
export async function getProjectDetail(
{ ref, skipWake = false }: ProjectDetailVariables,
signal?: AbortSignal,
headers?: Record<string, string>,
queryClient?: QueryClient
) {
if (!ref) throw new Error('Project ref is required')
const { data, error } = await get('/platform/projects/{ref}', {
params: { path: { ref } },
signal,
headers,
})
let connectionString = data?.connectionString
/**
* A project is marked as active but potentially hibernated and needs to be woken up (typically few seconds only).
* To prevent odd side effects like pg-meta queries failing or the likes, we wake up the project proactively and wait for it
* to be back online before returning the project details.
*/
if (data?.status === 'ACTIVE_HEALTHY' && data?.is_hibernating && !skipWake) {
// In case project was scaled down, explicitly wake it up before continuing to return the project details
const { error: errorWaking, data: wakeResponse } = await post('/platform/projects/{ref}/wake', {
params: { path: { ref } },
signal,
headers,
})
// We will still let the user load the page in case of a wake error, but log it for debugging purposes
if (errorWaking) {
console.error('Error waking up the project', { ref, errorWaking })
}
// As the instance IP has changed, we need to use the latest encrypted connection string
if (wakeResponse) {
connectionString = wakeResponse.connection_string
// If replicas were previously fetched, they will hold the old connection string that we need to replace
queryClient?.setQueryData<ReadReplicasData>(replicaKeys.list(ref), (old) => {
if (!old) return old
return old.map((db) => {
if (db.identifier === ref) {
return {
...db,
connectionString: wakeResponse.connection_string,
connection_string_read_only: wakeResponse.connection_string_read_only,
}
}
return db
})
})
}
}
if (error) handleError(error)
const project = { ...data, connectionString: connectionString } as Project
const statusOverride = getProjectStatusOverride(ref)
if (statusOverride) project.status = statusOverride
return project
}
export type ProjectDetailData = Awaited<ReturnType<typeof getProjectDetail>>
export type ProjectDetailError = ResponseError
export const useProjectDetailQuery = <TData = ProjectDetailData>(
{ ref }: ProjectDetailVariables,
{
enabled = true,
...options
}: UseCustomQueryOptions<ProjectDetailData, ProjectDetailError, TData> = {}
) => {
const queryClient = useQueryClient()
return useQuery<ProjectDetailData, ProjectDetailError, TData>({
queryKey: projectKeys.detail(ref),
queryFn: ({ signal }) => getProjectDetail({ ref }, signal, undefined, queryClient),
enabled: enabled && typeof ref !== 'undefined',
staleTime: 30 * 1000,
refetchInterval: (query) => {
const data = query.state.data
const status = data && data.status
const connectionString = data && data.connectionString
if (status === 'COMING_UP' || status === 'UNKNOWN' || !isValidConnString(connectionString)) {
return 5 * 1000 // 5 seconds
}
return false
},
...options,
})
}
export function prefetchProjectDetail(client: QueryClient, { ref }: ProjectDetailVariables) {
return client.fetchQuery({
queryKey: projectKeys.detail(ref),
queryFn: ({ client, signal }) =>
getProjectDetail({ ref, skipWake: true }, signal, undefined, client),
})
}
export const useInvalidateProjectDetailsQuery = () => {
const queryClient = useQueryClient()
const invalidateProjectDetailsQuery = useCallback(
(ref: string) => {
return queryClient.invalidateQueries({ queryKey: projectKeys.detail(ref) })
},
[queryClient]
)
return { invalidateProjectDetailsQuery }
}
export const useSetProjectPostgrestStatus = () => {
const queryClient = useQueryClient()
const setProjectPostgrestStatus = (ref: Project['ref'], status: Project['postgrestStatus']) => {
return queryClient.setQueriesData<Project>(
{ queryKey: projectKeys.detail(ref) },
(old) => {
if (!old) return old
return { ...old, postgrestStatus: status }
},
{ updatedAt: Date.now() }
)
}
return { setProjectPostgrestStatus }
}
export const useSetProjectStatus = () => {
const queryClient = useQueryClient()
const setProjectStatus = ({
ref,
slug,
status,
}: {
ref: Project['ref']
slug?: string
status: Project['status']
}) => {
// Org projects infinite query
if (slug) {
queryClient.setQueriesData<{ pages: OrgProjectsResponse[] } | undefined>(
{ queryKey: projectKeys.infiniteListByOrg(slug) },
(old) => {
if (!old) return old
return {
...old,
pages: old.pages.map((page) => {
return {
...page,
projects: page.projects.map((project) =>
project.ref === ref ? { ...project, status } : project
),
}
}),
}
},
{ updatedAt: Date.now() }
)
}
// Projects infinite query
queryClient.setQueriesData<{ pages: OrgProjectsResponse[] } | undefined>(
{ queryKey: projectKeys.infiniteList() },
(old) => {
if (!old) return old
return {
...old,
pages: old.pages.map((page) => {
return {
...page,
projects: page.projects.map((project) =>
project.ref === ref ? { ...project, status } : project
),
}
}),
}
},
{ updatedAt: Date.now() }
)
// Project details query
queryClient.setQueriesData<Project>(
{ queryKey: projectKeys.detail(ref) },
(old) => {
if (!old) return old
return { ...old, status }
},
{ updatedAt: Date.now() }
)
}
return { setProjectStatus }
}