Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ interface AddNewFactorModalProps {
onClose: () => void
}

const AddNewFactorModal = ({ visible, onClose }: AddNewFactorModalProps) => {
export const AddNewFactorModal = ({ visible, onClose }: AddNewFactorModalProps) => {
// Generate a name with a number between 0 and 1000
const [name, setName] = useState(`App ${Math.floor(Math.random() * 1000)}`)
const { data, mutate: enroll, isLoading: isEnrolling, reset } = useMfaEnrollMutation()
Expand Down Expand Up @@ -216,5 +216,3 @@ const SecondStep = ({
</ConfirmationModal>
)
}

export default AddNewFactorModal
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { useMfaListFactorsQuery } from 'data/profile/mfa-list-factors-query'
import { DATETIME_FORMAT } from 'lib/constants'
import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button } from 'ui'
import AddNewFactorModal from './AddNewFactorModal'
import { AddNewFactorModal } from './AddNewFactorModal'
import DeleteFactorModal from './DeleteFactorModal'

const TOTPFactors = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react
import { toast } from 'sonner'

import { executeSql } from 'data/sql/execute-sql-query'
import { configKeys } from 'data/config/keys'
import type { ResponseError } from 'types'
import { databaseExtensionsKeys } from './keys'

Expand Down Expand Up @@ -57,7 +58,10 @@ export const useDatabaseExtensionEnableMutation = ({
{
async onSuccess(data, variables, context) {
const { projectRef } = variables
await queryClient.invalidateQueries(databaseExtensionsKeys.list(projectRef))
await Promise.all([
queryClient.invalidateQueries(databaseExtensionsKeys.list(projectRef)),
queryClient.invalidateQueries(configKeys.upgradeEligibility(projectRef)),
])
await onSuccess?.(data, variables, context)
},
async onError(data, variables, context) {
Expand Down
31 changes: 13 additions & 18 deletions apps/studio/lib/api/self-hosted/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import {
DatabaseOperations,
ExecuteSqlOptions,
} from '@supabase/mcp-server-supabase/platform'
import { executeQuery } from './query'
import { applyAndTrackMigrations, listMigrationVersions } from './migrations'
import { executeQuery } from './query'

export type GetDatabaseOperationsOptions = {
headers?: HeadersInit
Expand All @@ -16,35 +16,30 @@ export function getDatabaseOperations({
return {
async executeSql<T>(_projectRef: string, options: ExecuteSqlOptions) {
const { query } = options
const response = await executeQuery({ query, headers })
const { data, error } = await executeQuery<T>({ query, headers })

if (response.error) {
const { code, message } = response.error
throw new Error(`Error executing SQL: ${message} (code: ${code})`)
if (error) {
throw error
}

return response as T
return data
},
async listMigrations() {
const response = await listMigrationVersions({ headers })
const { data, error } = await listMigrationVersions({ headers })

if (response.error) {
const { code, message } = response.error
throw new Error(`Error listing migrations: ${message} (code: ${code})`)
if (error) {
throw error
}

return response as any
return data
},
async applyMigration<T>(_projectRef: string, options: ApplyMigrationOptions) {
async applyMigration(_projectRef: string, options: ApplyMigrationOptions) {
const { query, name } = options
const response = await applyAndTrackMigrations({ query, name, headers })
const { error } = await applyAndTrackMigrations({ query, name, headers })

if (response.error) {
const { code, message } = response.error
throw new Error(`Error applying migration: ${message} (code: ${code})`)
if (error) {
throw error
}

return response as T
},
}
}
47 changes: 42 additions & 5 deletions apps/studio/lib/api/self-hosted/migrations.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { source } from 'common-tags'
import { makeRandomString } from 'lib/helpers'
import { executeQuery } from './query'
import { PgMetaDatabaseError, WrappedResult } from './types'
import { assertSelfHosted } from './util'

export type ListMigrationsResult = {
version: string
name?: string
}

const listMigrationVersionsQuery = () =>
'select version, name from supabase_migrations.schema_migrations order by version'
Expand Down Expand Up @@ -40,8 +47,31 @@ export type ListMigrationVersionsOptions = {
headers?: HeadersInit
}

export async function listMigrationVersions({ headers }: ListMigrationVersionsOptions) {
return await executeQuery({ query: listMigrationVersionsQuery(), headers })
/**
* Lists all migrations in the migrations history table.
*
* _Only call this from server-side self-hosted code._
*/
export async function listMigrationVersions({
headers,
}: ListMigrationVersionsOptions): Promise<WrappedResult<ListMigrationsResult[]>> {
assertSelfHosted()

const { data, error } = await executeQuery<ListMigrationsResult>({
query: listMigrationVersionsQuery(),
headers,
})

if (error) {
// Return empty list if the migrations table doesn't exist
if (error instanceof PgMetaDatabaseError && error.code === '42P01') {
return { data: [], error: undefined }
}

return { data: undefined, error }
}

return { data, error: undefined }
}

export type ApplyAndTrackMigrationsOptions = {
Expand All @@ -50,11 +80,18 @@ export type ApplyAndTrackMigrationsOptions = {
headers?: HeadersInit
}

export async function applyAndTrackMigrations({
/**
* Applies a SQL migration and tracks it in the migrations history table.
*
* _Only call this from server-side self-hosted code._
*/
export async function applyAndTrackMigrations<T = unknown>({
query,
name,
headers,
}: ApplyAndTrackMigrationsOptions) {
}: ApplyAndTrackMigrationsOptions): Promise<WrappedResult<T[]>> {
assertSelfHosted()

const initializeResponse = await executeQuery<void>({
query: initializeHistoryTableQuery(),
headers,
Expand All @@ -64,7 +101,7 @@ export async function applyAndTrackMigrations({
return initializeResponse
}

const applyAndTrackResponse = await executeQuery({
const applyAndTrackResponse = await executeQuery<T>({
query: applyAndTrackMigrationsQuery(query, name),
headers,
})
Expand Down
48 changes: 36 additions & 12 deletions apps/studio/lib/api/self-hosted/query.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,47 @@
import { fetchPost } from 'data/fetchers'
import { PG_META_URL } from 'lib/constants/index'
import { ResponseError } from 'types'
import { constructHeaders } from '../apiHelpers'
import { PgMetaDatabaseError, databaseErrorSchema, WrappedResult } from './types'
import { assertSelfHosted } from './util'

export type QueryOptions = {
query: string
headers?: HeadersInit
}

export async function executeQuery<T = unknown>({ query, headers }: QueryOptions) {
const response = await fetchPost<T[]>(
`${PG_META_URL}/query`,
{ query },
{ headers: constructHeaders(headers ?? {}) }
)
/**
* Executes a SQL query against the self-hosted Postgres instance via pg-meta service.
*
* _Only call this from server-side self-hosted code._
*/
export async function executeQuery<T = unknown>({
query,
headers,
}: QueryOptions): Promise<WrappedResult<T[]>> {
assertSelfHosted()

if (response instanceof ResponseError) {
return { error: response }
} else {
return { data: response }
const response = await fetch(`${PG_META_URL}/query`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...constructHeaders(headers ?? {}),
},
body: JSON.stringify({ query }),
})

try {
const result = await response.json()

if (!response.ok) {
const { message, code, formattedError } = databaseErrorSchema.parse(result)
const error = new PgMetaDatabaseError(message, code, response.status, formattedError)
return { data: undefined, error }
}

return { data: result, error: undefined }
} catch (error) {
if (error instanceof Error) {
return { data: undefined, error }
}
throw error
}
}
23 changes: 23 additions & 0 deletions apps/studio/lib/api/self-hosted/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import z from 'zod/v4'

export type WrappedSuccessResult<T> = { data: T; error: undefined }
export type WrappedErrorResult = { data: undefined; error: Error }
export type WrappedResult<R> = WrappedSuccessResult<R> | WrappedErrorResult

export const databaseErrorSchema = z.object({
message: z.string(),
code: z.string(),
formattedError: z.string(),
})

export class PgMetaDatabaseError extends Error {
constructor(
message: string,
public code: string,
public statusCode: number,
public formattedError: string
) {
super(message)
this.name = 'PgMetaDatabaseError'
}
}
10 changes: 10 additions & 0 deletions apps/studio/lib/api/self-hosted/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { IS_PLATFORM } from 'lib/constants'

/**
* Asserts that the current environment is self-hosted.
*/
export function assertSelfHosted() {
if (IS_PLATFORM) {
throw new Error('This function can only be called in self-hosted environments')
}
}
9 changes: 7 additions & 2 deletions apps/studio/pages/api/platform/pg-meta/[ref]/query/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { constructHeaders } from 'lib/api/apiHelpers'
import apiWrapper from 'lib/api/apiWrapper'
import { executeQuery } from 'lib/api/self-hosted/query'
import { PgMetaDatabaseError } from 'lib/api/self-hosted/types'
import { NextApiRequest, NextApiResponse } from 'next'

export default (req: NextApiRequest, res: NextApiResponse) =>
Expand All @@ -24,8 +25,12 @@ const handlePost = async (req: NextApiRequest, res: NextApiResponse) => {
const { data, error } = await executeQuery({ query, headers })

if (error) {
const { code, message } = error
return res.status(code ?? 500).json({ message, formattedError: message })
if (error instanceof PgMetaDatabaseError) {
const { statusCode, message, formattedError } = error
return res.status(statusCode).json({ message, formattedError })
}
const { message } = error
return res.status(500).json({ message, formattedError: message })
} else {
return res.status(200).json(data)
}
Expand Down
17 changes: 13 additions & 4 deletions apps/studio/pages/api/v1/projects/[ref]/database/migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { NextApiRequest, NextApiResponse } from 'next'
import { constructHeaders } from 'lib/api/apiHelpers'
import apiWrapper from 'lib/api/apiWrapper'
import { applyAndTrackMigrations, listMigrationVersions } from 'lib/api/self-hosted/migrations'
import { PgMetaDatabaseError } from 'lib/api/self-hosted/types'

export default (req: NextApiRequest, res: NextApiResponse) =>
apiWrapper(req, res, handler, { withAuth: true })
Expand All @@ -26,8 +27,12 @@ const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => {
const { data, error } = await listMigrationVersions(headers)

if (error) {
const { code, message } = error
return res.status(code ?? 500).json({ message })
if (error instanceof PgMetaDatabaseError) {
const { statusCode, message, formattedError } = error
return res.status(statusCode).json({ message, formattedError })
}
const { message } = error
return res.status(500).json({ message, formattedError: message })
} else {
return res.status(200).json(data)
}
Expand All @@ -40,8 +45,12 @@ const handlePost = async (req: NextApiRequest, res: NextApiResponse) => {
const { data, error } = await applyAndTrackMigrations({ query, name, headers })

if (error) {
const { code, message } = error
return res.status(code ?? 500).json({ message, formattedError: message })
if (error instanceof PgMetaDatabaseError) {
const { statusCode, message, formattedError } = error
return res.status(statusCode).json({ message, formattedError })
}
const { message } = error
return res.status(500).json({ message, formattedError: message })
} else {
return res.status(200).json(data)
}
Expand Down
3 changes: 0 additions & 3 deletions apps/studio/vercel.json

This file was deleted.

Loading