fix(local-mcp): database operations (#38965)
* fix(local-mcp): return values from database operations * fix(local-mcp): pg-meta status code forwarding
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
@@ -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 = {
|
||||
@@ -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,
|
||||
@@ -64,7 +101,7 @@ export async function applyAndTrackMigrations({
|
||||
return initializeResponse
|
||||
}
|
||||
|
||||
const applyAndTrackResponse = await executeQuery({
|
||||
const applyAndTrackResponse = await executeQuery<T>({
|
||||
query: applyAndTrackMigrationsQuery(query, name),
|
||||
headers,
|
||||
})
|
||||
|
||||
@@ -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
apps/studio/lib/api/self-hosted/types.ts
Normal file
23
apps/studio/lib/api/self-hosted/types.ts
Normal file
@@ -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
apps/studio/lib/api/self-hosted/util.ts
Normal file
10
apps/studio/lib/api/self-hosted/util.ts
Normal file
@@ -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')
|
||||
}
|
||||
}
|
||||
@@ -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) =>
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user