mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
## 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>
229 lines
7.0 KiB
TypeScript
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 }
|
|
}
|