fix: local api route fixes (#35642)

* fix: local api route fixes

* fix build

* fix tests

* address feedback

* remove unused debug
This commit is contained in:
Alaister Young authored and GitHub committed 2025-05-14 15:26:50 +00:00
1 parent bc4ca2e275
commit 6c83f97478
18 files changed
+67 -890

No files matched your search

@@ -6,6 +6,7 @@ import { useDebounce } from 'use-debounce'
import { useCompletion } from 'ai/react'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { useCronTimezoneQuery } from 'data/database-cron-jobs/database-cron-timezone-query'
import { constructHeaders } from 'data/fetchers'
import { BASE_PATH } from 'lib/constants'
import {
Accordion_Shadcn_,
@@ -78,7 +79,11 @@ export const CronJobScheduleSection = ({ form, supportsSeconds }: CronJobSchedul
useEffect(() => {
if (useNaturalLanguage && debouncedValue) {
generateCronSyntax(debouncedValue)
constructHeaders().then((headers) =>
generateCronSyntax(debouncedValue, {
headers: { Authorization: headers.get('Authorization') ?? '' },
})
)
return () => stop()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
+7 -259
View File
@@ -1,6 +1,5 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { apiAuthenticate } from './apiAuthenticate'
import { readOnly } from './supabaseClient'
const mocks = vi.hoisted(() => {
return {
@@ -11,25 +10,11 @@ const mocks = vi.hoisted(() => {
},
error: null,
}),
getIdentity: vi.fn().mockReturnValue({
identity: null,
error: null,
}),
getAuth0Id: vi.fn(),
}
})
// Mock dependencies
vi.mock('./supabaseClient', () => ({
readOnly: {
from: vi.fn(),
},
}))
vi.mock('lib/gotrue', () => ({
getAuthUser: mocks.getAuthUser,
getIdentity: mocks.getIdentity,
getAuth0Id: mocks.getAuth0Id,
}))
describe('apiAuthenticate', () => {
@@ -51,26 +36,12 @@ describe('apiAuthenticate', () => {
},
error: null,
})
mocks.getIdentity.mockReturnValue({
identity: null,
error: null,
})
})
it('should return error when request is not available', async () => {
const result = await apiAuthenticate(null as any, mockRes)
expect(result).toStrictEqual({ error: new Error('Request is not available') })
})
it('should return error when response is not available', async () => {
const result = await apiAuthenticate(mockReq, null as any)
expect(result).toStrictEqual({ error: new Error('Response is not available') })
})
it('should return error when authorization token is missing', async () => {
const reqWithoutToken = { ...mockReq, headers: {} }
const result = await apiAuthenticate(reqWithoutToken, mockRes)
expect(result).toStrictEqual({ error: { name: 'Error', message: 'missing access token' } })
expect(result).toStrictEqual({ error: new Error('missing access token') })
})
it('should return error when auth user fetch fails', async () => {
@@ -80,239 +51,16 @@ describe('apiAuthenticate', () => {
})
const result = await apiAuthenticate(mockReq, mockRes)
expect(result).toStrictEqual({ error: { name: 'Error', message: 'Auth failed' } })
})
it('should handle identity error', async () => {
mocks.getIdentity.mockReturnValue({
identity: null,
error: new Error('Identity error'),
})
const result = await apiAuthenticate(mockReq, mockRes)
expect(result).toStrictEqual({ error: { name: 'Error', message: 'Identity error' } })
})
it('should set auth0 id when identity provider is present', async () => {
mocks.getIdentity.mockReturnValue({
identity: {
provider: 'auth0',
id: 'auth0-id',
},
error: null,
})
mocks.getAuth0Id.mockReturnValue('auth0-user-id')
// Mock user query
vi.mocked(readOnly.from).mockReturnValue({
select: vi.fn().mockReturnThis(),
eq: vi.fn().mockReturnThis(),
single: vi
.fn()
.mockResolvedValue({ data: { id: 'test-user-id', primary_email: 'test@example.com' } }),
} as any)
const result = await apiAuthenticate(mockReq, mockRes)
expect(result).toStrictEqual({
id: 'test-user-id',
primary_email: 'test@example.com',
})
expect(mocks.getAuth0Id).toHaveBeenCalledWith('auth0', 'auth0-id')
})
it('should return user when user_id_supabase is present', async () => {
// Mock identity to return auth0 provider
mocks.getIdentity.mockReturnValue({
identity: {
provider: 'auth0',
id: 'auth0-id',
},
error: null,
})
mocks.getAuth0Id.mockReturnValue('auth0-user-id')
// Mock user query to return a user with auth0_id
vi.mocked(readOnly.from).mockReturnValue({
select: vi.fn().mockReturnThis(),
eq: vi.fn().mockReturnThis(),
single: vi.fn().mockResolvedValue({
data: {
id: 'supabase-user-id',
auth0_id: 'auth0-user-id',
primary_email: 'test@example.com',
},
}),
} as any)
const result = await apiAuthenticate(mockReq, mockRes)
expect(result).toStrictEqual({
id: 'supabase-user-id',
auth0_id: 'auth0-user-id',
primary_email: 'test@example.com',
})
expect(result).toStrictEqual({ error: new Error('Auth failed') })
})
it('should return error when user does not exist', async () => {
vi.mocked(readOnly.from).mockReturnValue({
select: vi.fn().mockReturnThis(),
eq: vi.fn().mockReturnThis(),
single: vi.fn().mockResolvedValue({ data: null }),
} as any)
mocks.getAuthUser.mockResolvedValue({
user: null,
error: null,
})
const result = await apiAuthenticate(mockReq, mockRes)
expect(result).toStrictEqual({ error: new Error('The user does not exist') })
})
it('should check organization permissions when orgSlug is provided', async () => {
const reqWithOrg = {
...mockReq,
query: { slug: 'test-org' },
}
// Mock user query
vi.mocked(readOnly.from).mockReturnValueOnce({
select: vi.fn().mockReturnThis(),
eq: vi.fn().mockReturnThis(),
single: vi
.fn()
.mockResolvedValue({ data: { id: 'test-user-id', primary_email: 'test@example.com' } }),
} as any)
// Mock organization query
vi.mocked(readOnly.from).mockReturnValueOnce({
select: vi.fn().mockReturnThis(),
match: vi.fn().mockReturnThis(),
single: vi.fn().mockResolvedValue({ data: { id: 'org-id' } }),
} as any)
// Mock member check
vi.mocked(readOnly.from).mockReturnValueOnce({
select: vi.fn().mockReturnThis(),
match: vi.fn().mockReturnThis(),
single: vi.fn().mockResolvedValue({ data: { id: 'member-id' }, status: 200 }),
} as any)
const result = await apiAuthenticate(reqWithOrg, mockRes)
expect(result).toStrictEqual({
id: 'test-user-id',
primary_email: 'test@example.com',
})
})
it('should return error when user lacks organization permissions', async () => {
const reqWithOrg = {
...mockReq,
query: { slug: 'test-org' },
}
// Mock user query
vi.mocked(readOnly.from).mockReturnValueOnce({
select: vi.fn().mockReturnThis(),
eq: vi.fn().mockReturnThis(),
single: vi
.fn()
.mockResolvedValue({ data: { id: 'test-user-id', primary_email: 'test@example.com' } }),
} as any)
// Mock organization query
vi.mocked(readOnly.from).mockReturnValueOnce({
select: vi.fn().mockReturnThis(),
match: vi.fn().mockReturnThis(),
single: vi.fn().mockResolvedValue({ data: { id: 'org-id' } }),
} as any)
// Mock member check failure
vi.mocked(readOnly.from).mockReturnValueOnce({
select: vi.fn().mockReturnThis(),
match: vi.fn().mockReturnThis(),
single: vi.fn().mockRejectedValue(new Error('Permission denied')),
} as any)
const result = await apiAuthenticate(reqWithOrg, mockRes)
expect(result).toStrictEqual({
error: { name: 'Error', message: 'The user does not have permission' },
})
})
it('should handle unknown errors gracefully', async () => {
mocks.getAuthUser.mockRejectedValue(new Error('Unexpected error'))
const result = await apiAuthenticate(mockReq, mockRes)
expect(result).toStrictEqual({ error: { name: 'Error', message: 'Unexpected error' } })
})
it('should get organization from project reference', async () => {
const reqWithProject = {
...mockReq,
query: { ref: 'test-project' },
}
// Mock user query
vi.mocked(readOnly.from).mockReturnValueOnce({
select: vi.fn().mockReturnThis(),
eq: vi.fn().mockReturnThis(),
single: vi
.fn()
.mockResolvedValue({ data: { id: 'test-user-id', primary_email: 'test@example.com' } }),
} as any)
// Mock project query
vi.mocked(readOnly.from).mockReturnValueOnce({
select: vi.fn().mockReturnThis(),
match: vi.fn().mockReturnThis(),
single: vi.fn().mockResolvedValue({ data: { organization_id: 'org-id' } }),
} as any)
// Mock member check
vi.mocked(readOnly.from).mockReturnValueOnce({
select: vi.fn().mockReturnThis(),
match: vi.fn().mockReturnThis(),
single: vi.fn().mockResolvedValue({ data: { id: 'member-id' }, status: 200 }),
} as any)
const result = await apiAuthenticate(reqWithProject, mockRes)
expect(result).toStrictEqual({
id: 'test-user-id',
primary_email: 'test@example.com',
})
})
it('should use organization_id from projectRef in member check', async () => {
const reqWithProject = {
...mockReq,
query: { ref: 'project-xyz' },
}
// Mock user query
vi.mocked(readOnly.from).mockReturnValueOnce({
select: vi.fn().mockReturnThis(),
eq: vi.fn().mockReturnThis(),
single: vi
.fn()
.mockResolvedValue({ data: { id: 'user-123', primary_email: 'user@example.com' } }),
} as any)
// Mock project query to return a specific organization_id
vi.mocked(readOnly.from).mockReturnValueOnce({
select: vi.fn().mockReturnThis(),
match: vi.fn().mockReturnThis(),
single: vi.fn().mockResolvedValue({ data: { organization_id: 'org-abc' } }),
} as any)
// Spy on the match function for the member check
const matchSpy = vi.fn().mockReturnThis()
vi.mocked(readOnly.from).mockReturnValueOnce({
select: vi.fn().mockReturnThis(),
match: matchSpy,
single: vi.fn().mockResolvedValue({ data: { id: 'member-1' }, status: 200 }),
} as any)
const result = await apiAuthenticate(reqWithProject, mockRes)
expect(result).toStrictEqual({
id: 'user-123',
primary_email: 'user@example.com',
})
// Ensure the member check used the org id from the project lookup
expect(matchSpy).toHaveBeenCalledWith({ organization_id: 'org-abc', user_id: 'user-123' })
})
})
+15 -103
View File
@@ -1,44 +1,30 @@
import { getAuthUser } from 'lib/gotrue'
import type { NextApiRequest, NextApiResponse } from 'next'
import { readOnly } from './supabaseClient'
import type { SupaResponse, User } from 'types'
import { getAuth0Id, getAuthUser, getIdentity } from 'lib/gotrue'
import type { ResponseError, SupaResponse, User } from 'types'
/**
* Use this method on api routes to check if user is authenticated and having required permissions.
* This method can only be used from the server side.
* Member permission is mandatory whenever orgSlug/projectRef query param exists
* @param {NextApiRequest} req
* @param {NextApiResponse} res
* @param {Object} config requireUserDetail: bool, requireOwner: bool
* @param {NextApiResponse} _res
*
* @returns {Object<user, error, description>}
* user null, with error and description if not authenticated or not enough permissions
*/
export async function apiAuthenticate(
req: NextApiRequest,
res: NextApiResponse
_res: NextApiResponse
): Promise<SupaResponse<User>> {
if (!req) {
return { error: new Error('Request is not available') }
}
if (!res) {
return { error: new Error('Response is not available') }
}
const { slug: orgSlug, ref: projectRef } = req.query
try {
const user = await fetchUser(req, res)
const user = await fetchUser(req)
if (!user) {
return { error: new Error('The user does not exist') }
}
if (orgSlug || projectRef) await checkMemberPermission(req, user)
return user
} catch (error: any) {
console.error('Error at apiAuthenticate', error)
return { error: { name: 'Error', message: error.message ?? 'unknown' } }
} catch (error) {
return { error: error as ResponseError }
}
}
@@ -46,93 +32,19 @@ export async function apiAuthenticate(
* @returns
* user with only id prop or detail object. It depends on requireUserDetail config
*/
async function fetchUser(req: NextApiRequest, res: NextApiResponse): Promise<any> {
let user_id_supabase = null
let user_id_auth0 = null
let gotrue_id = null
let email = null
const token = req.headers.authorization
async function fetchUser(req: NextApiRequest): Promise<any> {
const token = req.headers.authorization?.replace('Bearer ', '')
if (!token) {
throw new Error('missing access token')
}
let { user: gotrue_user, error: authError } = await getAuthUser(token)
if (authError) {
throw authError
}
if (gotrue_user !== null) {
gotrue_id = gotrue_user?.id
email = gotrue_user.email
let { identity, error } = getIdentity(gotrue_user)
if (error) throw error
if (identity?.provider !== undefined) {
user_id_auth0 = getAuth0Id(identity?.provider, identity?.id)
}
const { user, error } = await getAuthUser(token)
if (error) {
throw error
}
if (user_id_supabase) {
return {
id: user_id_supabase,
primary_email: email,
}
if (!user) {
throw new Error('The user does not exist')
}
const query = readOnly.from('users').select(
`
id, auth0_id, primary_email, username, first_name, last_name, mobile, is_alpha_user
`
)
const { data } = await query.eq('gotrue_id', gotrue_id).single()
return data
}
async function checkMemberPermission(req: NextApiRequest, user: any) {
const org = await getOrganization(req)
if (!org) {
throw new Error('User organization does not exist')
}
try {
const response = await readOnly
.from('members')
.select('id')
.match({ organization_id: org.id, user_id: user.id })
.single()
if (!response || response.status != 200) {
throw new Error('The user does not have permission')
}
return true
} catch (error) {
throw new Error('The user does not have permission')
}
}
async function getOrganization(req: NextApiRequest) {
const { slug: orgSlug, ref: projectRef } = req.query
if (!orgSlug && !projectRef) {
throw new Error('Not enough info to check user permissions')
}
if (orgSlug) {
const { data } = await readOnly
.from('organizations')
.select('id')
.match({ slug: orgSlug })
.single()
return { id: data.id }
}
if (projectRef) {
const { data } = await readOnly
.from('projects')
.select('organization_id')
.match({ ref: projectRef })
.single()
return { id: data.organization_id }
}
return null
return user
}
@@ -1,49 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { readOnly } from './supabaseClient'
vi.mock('lib/constants', () => ({
IS_PLATFORM: true,
}))
const readOnlyErrMessage = 'Read only error'
vi.mock('@supabase/supabase-js', () => ({
createClient: vi.fn(() => ({
from: vi.fn(() => ({
insert: () => {
throw readOnlyErrMessage
},
delete: () => {
throw readOnlyErrMessage
},
update: () => {
throw readOnlyErrMessage
},
})),
rpc: () => {
throw readOnlyErrMessage
},
})),
}))
describe('supabaseClient', () => {
it('should be defined', () => {
expect(readOnly).toBeDefined()
})
it('should throw on inserts', () => {
expect(() => readOnly.from('').insert({})).toThrowError()
})
it('should throw on deletes', () => {
expect(() => readOnly.from('').delete({})).toThrowError()
})
it('should throw on updates', () => {
expect(() => readOnly.from('').update({})).toThrowError()
})
it('should throw on rpc', () => {
expect(() => readOnly.rpc({})).toThrowError()
})
})
-26
View File
@@ -1,26 +0,0 @@
import { createClient } from '@supabase/supabase-js'
import { IS_PLATFORM } from '../constants'
let readOnly: any
if (IS_PLATFORM) {
readOnly = createClient(process.env.READ_ONLY_URL ?? '', process.env.READ_ONLY_API_KEY ?? '')
const readOnlyErrMessage = Error('This client is for read-only actions. Use readWrite instead.')
// overwrites function calls
// for readOnly
readOnly.from('').insert = () => {
throw readOnlyErrMessage
}
readOnly.from('').delete = () => {
throw readOnlyErrMessage
}
readOnly.from('').update = () => {
throw readOnlyErrMessage
}
readOnly.rpc = () => {
throw readOnlyErrMessage
}
}
export { readOnly }
+2 -1
View File
@@ -1,4 +1,5 @@
import { getAccessToken, gotrueClient, type User } from 'common'
import { getAccessToken, type User } from 'common/auth'
import { gotrueClient } from 'common/gotrue'
export const auth = gotrueClient
export { getAccessToken }
-4
View File
@@ -7,16 +7,12 @@ export const config = {
// [Joshen] Return 404 for all next.js API endpoints EXCEPT the ones we use in hosted:
const HOSTED_SUPPORTED_API_URLS = [
'/ai/sql/suggest',
'/ai/sql/generate-v2',
'/ai/sql/generate-v3',
'/ai/edge-function/complete',
'/ai/onboarding/design',
'/ai/sql/complete',
'/ai/sql/title',
'/ai/sql/debug',
'/ai/sql/cron',
'/ai/docs',
'/get-ip-address',
'/get-utc-time',
'/edge-functions/test',
-150
View File
@@ -1,150 +0,0 @@
import { SupabaseClient } from '@supabase/supabase-js'
import { ApplicationError, UserError, clippy } from 'ai-commands/edge'
import { NextRequest } from 'next/server'
import OpenAI from 'openai'
export const config = {
runtime: 'edge',
/* To avoid OpenAI errors, restrict to the Vercel Edge Function regions that
overlap with the OpenAI API regions.
Reference for Vercel regions: https://vercel.com/docs/edge-network/regions#region-list
Reference for OpenAI regions: https://help.openai.com/en/articles/5347006-openai-api-supported-countries-and-territories
*/
regions: [
'arn1',
'bom1',
'cdg1',
'cle1',
'cpt1',
'dub1',
'fra1',
'gru1',
'hnd1',
'iad1',
'icn1',
'kix1',
'lhr1',
'pdx1',
'sfo1',
'sin1',
'syd1',
],
}
const openAiKey = process.env.OPENAI_API_KEY
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL as string
const supabaseServiceKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY as string
export default async function handler(req: NextRequest) {
if (!openAiKey) {
return new Response(
JSON.stringify({
error: 'No OPENAI_API_KEY set. Create this environment variable to use AI features.',
}),
{
status: 500,
headers: { 'Content-Type': 'application/json' },
}
)
}
if (!supabaseUrl) {
return new Response(
JSON.stringify({
error:
'No NEXT_PUBLIC_SUPABASE_URL set. Create this environment variable to use AI features.',
}),
{
status: 500,
headers: { 'Content-Type': 'application/json' },
}
)
}
if (!supabaseServiceKey) {
return new Response(
JSON.stringify({
error:
'No NEXT_PUBLIC_SUPABASE_ANON_KEY set. Create this environment variable to use AI features.',
}),
{
status: 500,
headers: { 'Content-Type': 'application/json' },
}
)
}
const { method } = req
switch (method) {
case 'POST':
return handlePost(req)
default:
return new Response(
JSON.stringify({ data: null, error: { message: `Method ${method} Not Allowed` } }),
{
status: 405,
headers: { 'Content-Type': 'application/json', Allow: 'POST' },
}
)
}
}
async function handlePost(request: NextRequest) {
const openai = new OpenAI({ apiKey: openAiKey })
const body = await (request.json() as Promise<{
messages: { content: string; role: 'user' | 'assistant' }[]
}>)
const { messages } = body
if (!messages) {
throw new UserError('Missing messages in request data')
}
const supabaseClient = new SupabaseClient(supabaseUrl, supabaseServiceKey)
try {
const response = await clippy(openai, supabaseClient, messages)
// Proxy the streamed SSE response from OpenAI
return new Response(response.body, {
headers: {
'Content-Type': 'text/event-stream',
},
})
} catch (error: unknown) {
console.error(error)
if (error instanceof UserError) {
return new Response(
JSON.stringify({
error: error.message,
data: error.data,
}),
{
status: 400,
headers: { 'Content-Type': 'application/json' },
}
)
} else if (error instanceof ApplicationError) {
// Print out application errors with their additional data
console.error(`${error.message}: ${JSON.stringify(error.data)}`)
} else {
// Print out unexpected errors as is to help with debugging
console.error(error)
}
// TODO: include more response info in debug environments
return new Response(
JSON.stringify({
error: 'There was an error processing your request',
}),
{
status: 500,
headers: { 'Content-Type': 'application/json' },
}
)
}
}
@@ -2,6 +2,7 @@ import { openai } from '@ai-sdk/openai'
import pgMeta from '@supabase/pg-meta'
import { streamText } from 'ai'
import { executeSql } from 'data/sql/execute-sql-query'
import apiWrapper from 'lib/api/apiWrapper'
import { NextApiRequest, NextApiResponse } from 'next'
import { getTools } from '../sql/tools'
@@ -9,7 +10,7 @@ export const maxDuration = 30
const openAiKey = process.env.OPENAI_API_KEY
const pgMetaSchemasList = pgMeta.schemas.list()
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!openAiKey) {
return new Response(
JSON.stringify({
@@ -35,6 +36,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
}
const wrapper = (req: NextApiRequest, res: NextApiResponse) =>
apiWrapper(req, res, handler, { withAuth: true })
export default wrapper
async function handlePost(req: NextApiRequest, res: NextApiResponse) {
try {
const { completionMetadata, projectRef, connectionString, includeSchemaMetadata } = req.body
@@ -1,7 +1,7 @@
import { openai } from '@ai-sdk/openai'
import { streamText } from 'ai'
import { streamText, tool } from 'ai'
import apiWrapper from 'lib/api/apiWrapper'
import { NextApiRequest, NextApiResponse } from 'next'
import { tool } from 'ai'
import { z } from 'zod'
const openAiKey = process.env.OPENAI_API_KEY
@@ -46,7 +46,7 @@ const getTools = () => {
}
}
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!openAiKey) {
return res.status(400).json({
error: 'No OPENAI_API_KEY set. Create this environment variable to use AI features.',
@@ -64,6 +64,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
}
const wrapper = (req: NextApiRequest, res: NextApiResponse) =>
apiWrapper(req, res, handler, { withAuth: true })
export default wrapper
async function handlePost(req: NextApiRequest, res: NextApiResponse) {
const { messages } = req.body
@@ -1,7 +1,10 @@
import apiWrapper from 'lib/api/apiWrapper'
import { NextApiRequest, NextApiResponse } from 'next'
export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
const wrapper = (req: NextApiRequest, res: NextApiResponse) =>
apiWrapper(req, res, handler, { withAuth: true })
export default wrapper
async function handler(req: NextApiRequest, res: NextApiResponse) {
const { method } = req
+7 -1
View File
@@ -2,6 +2,7 @@ import { openai } from '@ai-sdk/openai'
import pgMeta from '@supabase/pg-meta'
import { streamText } from 'ai'
import { executeSql } from 'data/sql/execute-sql-query'
import apiWrapper from 'lib/api/apiWrapper'
import { NextApiRequest, NextApiResponse } from 'next'
import { getTools } from '../sql/tools'
@@ -9,7 +10,7 @@ export const maxDuration = 30
const openAiKey = process.env.OPENAI_API_KEY
const pgMetaSchemasList = pgMeta.schemas.list()
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!openAiKey) {
return new Response(
JSON.stringify({
@@ -150,3 +151,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
})
}
}
const wrapper = (req: NextApiRequest, res: NextApiResponse) =>
apiWrapper(req, res, handler, { withAuth: true })
export default wrapper
+2 -1
View File
@@ -54,6 +54,7 @@ export async function handlePost(req: NextApiRequest, res: NextApiResponse) {
}
}
const wrapper = (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
const wrapper = (req: NextApiRequest, res: NextApiResponse) =>
apiWrapper(req, res, handler, { withAuth: true })
export default wrapper
-80
View File
@@ -1,80 +0,0 @@
import { ContextLengthError, EmptySqlError, debugSql } from 'ai-commands'
import apiWrapper from 'lib/api/apiWrapper'
import { NextApiRequest, NextApiResponse } from 'next'
import { OpenAI } from 'openai'
const openAiKey = process.env.OPENAI_API_KEY
const openai = new OpenAI({ apiKey: openAiKey })
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!openAiKey) {
return res.status(500).json({
error: 'No OPENAI_API_KEY set. Create this environment variable to use AI features.',
})
}
const { method } = req
switch (method) {
case 'POST':
return handlePost(req, res)
default:
res.setHeader('Allow', ['POST'])
res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } })
}
}
export async function handlePost(req: NextApiRequest, res: NextApiResponse) {
const {
body: { errorMessage, sql, entityDefinitions },
} = req
try {
const result = await debugSql(openai, errorMessage, sql, entityDefinitions)
return res.json(result)
} catch (error) {
if (error instanceof Error) {
console.error(`AI SQL debugging failed: ${error.message}`)
const hasEntityDefinitions = entityDefinitions !== undefined && entityDefinitions.length > 0
if (error instanceof ContextLengthError) {
// If there are more entity definitions than the SQL provided, attribute the
// error to the database metadata
if (hasEntityDefinitions) {
const definitionsLength = entityDefinitions.reduce(
(sum: number, def: string) => sum + def.length,
0
)
if (definitionsLength > sql.length) {
return res.status(400).json({
error:
'Your database metadata is too large for Supabase AI to ingest. Try disabling database metadata in AI settings.',
})
}
}
// Otherwise attribute the error to the SQL being too large
return res.status(400).json({
error:
'Your SQL query is too large for Supabase AI to ingest. Try splitting it into smaller queries.',
})
}
if (error instanceof EmptySqlError) {
res.status(400).json({
error: 'Unable to debug SQL. No fix identified for the error.',
})
}
} else {
console.log(`Unknown error: ${error}`)
}
return res.status(500).json({
error: 'There was an unknown error debugging the SQL snippet. Please try again.',
})
}
}
const wrapper = (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
export default wrapper
-107
View File
@@ -1,107 +0,0 @@
import { StreamingTextResponse } from 'ai'
import { chatRlsPolicy, chatSql } from 'ai-commands/edge'
import { SupportedAssistantEntities } from 'components/ui/AIAssistantPanel/AIAssistant.types'
import { DatabasePoliciesData } from 'data/database-policies/database-policies-query'
import { NextRequest } from 'next/server'
import OpenAI from 'openai'
export const config = {
runtime: 'edge',
/* To avoid OpenAI errors, restrict to the Vercel Edge Function regions that
overlap with the OpenAI API regions.
Reference for Vercel regions: https://vercel.com/docs/edge-network/regions#region-list
Reference for OpenAI regions: https://help.openai.com/en/articles/5347006-openai-api-supported-countries-and-territories
*/
regions: [
'arn1',
'bom1',
'cdg1',
'cle1',
'cpt1',
'dub1',
'fra1',
'gru1',
'hnd1',
'iad1',
'icn1',
'kix1',
'lhr1',
'pdx1',
'sfo1',
'sin1',
'syd1',
],
}
const openAiKey = process.env.OPENAI_API_KEY
export default async function handler(req: NextRequest) {
if (!openAiKey) {
return new Response(
JSON.stringify({
error: 'No OPENAI_API_KEY set. Create this environment variable to use AI features.',
}),
{
status: 500,
headers: { 'Content-Type': 'application/json' },
}
)
}
const { method } = req
switch (method) {
case 'POST':
return handlePost(req)
default:
return new Response(
JSON.stringify({ data: null, error: { message: `Method ${method} Not Allowed` } }),
{
status: 405,
headers: { 'Content-Type': 'application/json', Allow: 'POST' },
}
)
}
}
async function handlePost(request: NextRequest) {
const openai = new OpenAI({ apiKey: openAiKey })
const body = await (request.json() as Promise<{
context?: SupportedAssistantEntities
messages: { content: string; role: 'user' | 'assistant' }[]
existingSql?: string
entityDefinitions: string[]
existingPolicies?: DatabasePoliciesData
}>)
const { messages, existingSql, entityDefinitions, context, existingPolicies } = body
try {
if (context === 'rls-policies') {
const stream = await chatRlsPolicy(
openai,
messages,
entityDefinitions,
existingPolicies ?? [],
existingSql
)
return new StreamingTextResponse(stream)
} else {
const stream = await chatSql(openai, messages, existingSql, entityDefinitions, context)
return new StreamingTextResponse(stream)
}
} catch (error) {
if (error instanceof Error) {
console.error(`AI SQL generation-v2 failed: ${error.message}`)
} else {
console.error(`AI SQL generation-v2 failed: ${error}`)
}
return new Response(JSON.stringify({ error: 'There was an error processing your request' }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
})
}
}
+7 -1
View File
@@ -4,13 +4,14 @@ import { streamText } from 'ai'
import { NextApiRequest, NextApiResponse } from 'next'
import { executeSql } from 'data/sql/execute-sql-query'
import apiWrapper from 'lib/api/apiWrapper'
import { getTools } from './tools'
export const maxDuration = 30
const openAiKey = process.env.OPENAI_API_KEY
const pgMetaSchemasList = pgMeta.schemas.list()
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!openAiKey) {
return res.status(500).json({
error: 'No OPENAI_API_KEY set. Create this environment variable to use AI features.',
@@ -28,6 +29,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
}
const wrapper = (req: NextApiRequest, res: NextApiResponse) =>
apiWrapper(req, res, handler, { withAuth: true })
export default wrapper
async function handlePost(req: NextApiRequest, res: NextApiResponse) {
const { messages, projectRef, connectionString, includeSchemaMetadata, schema, table } = req.body
-101
View File
@@ -1,101 +0,0 @@
import { StreamingTextResponse } from 'ai'
import { chatRlsPolicy } from 'ai-commands/edge'
import { DatabasePoliciesData } from 'data/database-policies/database-policies-query'
import { NextRequest } from 'next/server'
import OpenAI from 'openai'
export const config = {
runtime: 'edge',
/* To avoid OpenAI errors, restrict to the Vercel Edge Function regions that
overlap with the OpenAI API regions.
Reference for Vercel regions: https://vercel.com/docs/edge-network/regions#region-list
Reference for OpenAI regions: https://help.openai.com/en/articles/5347006-openai-api-supported-countries-and-territories
*/
regions: [
'arn1',
'bom1',
'cdg1',
'cle1',
'cpt1',
'dub1',
'fra1',
'gru1',
'hnd1',
'iad1',
'icn1',
'kix1',
'lhr1',
'pdx1',
'sfo1',
'sin1',
'syd1',
],
}
const openAiKey = process.env.OPENAI_API_KEY
export default async function handler(req: NextRequest) {
if (!openAiKey) {
return new Response(
JSON.stringify({
error: 'No OPENAI_API_KEY set. Create this environment variable to use AI features.',
}),
{
status: 500,
headers: { 'Content-Type': 'application/json' },
}
)
}
const { method } = req
switch (method) {
case 'POST':
return handlePost(req)
default:
return new Response(
JSON.stringify({ data: null, error: { message: `Method ${method} Not Allowed` } }),
{
status: 405,
headers: { 'Content-Type': 'application/json', Allow: 'POST' },
}
)
}
}
async function handlePost(request: NextRequest) {
const openai = new OpenAI({ apiKey: openAiKey })
const body = await (request.json() as Promise<{
messages: { content: string; role: 'user' | 'assistant' }[]
entityDefinitions: string[]
existingPolicies: DatabasePoliciesData
policyDefinition: string
}>)
const { messages, entityDefinitions, existingPolicies, policyDefinition } = body
try {
const stream = await chatRlsPolicy(
openai,
messages,
entityDefinitions,
existingPolicies,
policyDefinition
)
return new StreamingTextResponse(stream)
} catch (error) {
console.error(error)
return new Response(
JSON.stringify({
error: 'There was an error processing your request',
}),
{
status: 500,
headers: { 'Content-Type': 'application/json' },
}
)
}
}
+2 -1
View File
@@ -52,6 +52,7 @@ export async function handlePost(req: NextApiRequest, res: NextApiResponse) {
}
}
const wrapper = (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
const wrapper = (req: NextApiRequest, res: NextApiResponse) =>
apiWrapper(req, res, handler, { withAuth: true })
export default wrapper