mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
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:
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
|
||||
|
||||
@@ -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' })
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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' },
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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' },
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
Reference in new issue
Block a user