mirror of
https://github.com/supabase/supabase.git
synced 2026-10-15 06:09:08 +03:00
feat: use parsed function responses from mgmt-api
This commit is contained in:
1 parent
37ee1c0ca2
commit
ed70a2bd85
5 files changed
+44
-254
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { getMultipartBoundary, parseMultipart } from '@mjackson/multipart-parser'
|
||||
import { constructHeaders, handleError } from 'data/fetchers'
|
||||
import { BASE_PATH, IS_PLATFORM } from 'lib/constants'
|
||||
import { API_URL, BASE_PATH, IS_PLATFORM } from 'lib/constants'
|
||||
import { ResponseError } from 'types'
|
||||
import { edgeFunctionsKeys } from './keys'
|
||||
|
||||
@@ -18,6 +19,26 @@ export type EdgeFunctionBodyResponse = {
|
||||
files: EdgeFunctionFile[]
|
||||
}
|
||||
|
||||
async function streamToString(stream: ReadableStream<Uint8Array>) {
|
||||
const reader = stream.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let result = ''
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
result += decoder.decode(value, { stream: true })
|
||||
}
|
||||
// Final decode to handle any remaining bytes
|
||||
result += decoder.decode()
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('Error reading stream:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export async function getEdgeFunctionBody(
|
||||
{ projectRef, slug }: EdgeFunctionBodyVariables,
|
||||
signal?: AbortSignal
|
||||
@@ -28,20 +49,22 @@ export async function getEdgeFunctionBody(
|
||||
try {
|
||||
// Get authorization headers
|
||||
const headers = await constructHeaders({
|
||||
'Content-Type': 'application/json',
|
||||
//'Content-Type': 'application/json',
|
||||
Accept: 'multipart/form-data',
|
||||
})
|
||||
|
||||
// Send to our API for processing (the API will handle the fetch from v1 endpoint)
|
||||
const parseResponse = await fetch(`${BASE_PATH}/api/edge-functions/body`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ projectRef, slug }),
|
||||
const baseUrl = API_URL?.replace('/platform', '')
|
||||
const response = await fetch(`${baseUrl}/v1/projects/${projectRef}/functions/${slug}/body`, {
|
||||
method: 'GET',
|
||||
//body: JSON.stringify({ projectRef, slug }),
|
||||
headers,
|
||||
credentials: 'include',
|
||||
signal,
|
||||
})
|
||||
|
||||
if (!parseResponse.ok) {
|
||||
const { error } = await parseResponse.json()
|
||||
if (!response.ok) {
|
||||
const { error } = await response.json()
|
||||
handleError(
|
||||
typeof error === 'object'
|
||||
? error
|
||||
@@ -51,7 +74,17 @@ export async function getEdgeFunctionBody(
|
||||
)
|
||||
}
|
||||
|
||||
const { files } = await parseResponse.json()
|
||||
const boundary = getMultipartBoundary(response.headers.get('content-type'))
|
||||
const files = []
|
||||
|
||||
await parseMultipart(response.body, { boundary }, async (part) => {
|
||||
if (part.isFile) {
|
||||
files.push({
|
||||
name: part.filename,
|
||||
content: await streamToString(part.body),
|
||||
})
|
||||
}
|
||||
})
|
||||
return files as EdgeFunctionFile[]
|
||||
} catch (error) {
|
||||
console.error('Failed to parse edge function code:', error)
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
import { Parser } from '@deno/eszip'
|
||||
import path from 'path'
|
||||
|
||||
function url2path(url: string) {
|
||||
try {
|
||||
// Parse the URL
|
||||
return new URL(url).pathname
|
||||
} catch (error) {
|
||||
// If URL parsing fails, fallback to extracting just the filename
|
||||
console.warn('Failed to parse URL:', url)
|
||||
try {
|
||||
// Try to extract just the filename part
|
||||
const parts = url.split('/').filter(Boolean)
|
||||
if (parts.length > 0) {
|
||||
return parts[parts.length - 1] // Return just the filename
|
||||
}
|
||||
} catch (e) {
|
||||
// Last resort: use the original path joining
|
||||
console.error('Failed to extract filename:', e)
|
||||
}
|
||||
return path.join(...new URL(url).pathname.split('/').filter(Boolean))
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize parser outside of request handler
|
||||
let parserPromise: Promise<any> | null = null
|
||||
|
||||
async function getParser() {
|
||||
if (!parserPromise) {
|
||||
parserPromise = Parser.createInstance().catch((err) => {
|
||||
console.error('Failed to create parser instance:', err)
|
||||
parserPromise = null
|
||||
throw err
|
||||
})
|
||||
}
|
||||
return parserPromise
|
||||
}
|
||||
|
||||
export async function parseEszip(bytes: Uint8Array) {
|
||||
try {
|
||||
const parser = await getParser()
|
||||
if (!parser) {
|
||||
throw new Error('Failed to initialize parser')
|
||||
}
|
||||
|
||||
// Parse bytes in a try-catch block
|
||||
let specifiers: string[] = []
|
||||
try {
|
||||
specifiers = await parser.parseBytes(bytes)
|
||||
} catch (parseError) {
|
||||
console.error('Error parsing bytes:', parseError)
|
||||
// Reset parser on parse error
|
||||
parserPromise = null
|
||||
throw parseError
|
||||
}
|
||||
|
||||
// Load in a separate try-catch
|
||||
try {
|
||||
await parser.load()
|
||||
} catch (loadError) {
|
||||
console.error('Error loading parser:', loadError)
|
||||
parserPromise = null
|
||||
throw loadError
|
||||
}
|
||||
|
||||
// Extract files from the eszip
|
||||
const files = await extractEszip(parser, specifiers)
|
||||
|
||||
// Convert files to the expected format
|
||||
const responseFiles = await Promise.all(
|
||||
files.map(async (file) => {
|
||||
const content = await file.text()
|
||||
return {
|
||||
name: file.name,
|
||||
content: content,
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return responseFiles
|
||||
} catch (error) {
|
||||
console.error('Error in parseEszip:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function extractEszip(parser: any, specifiers: string[]) {
|
||||
const files = []
|
||||
|
||||
// First, filter out the specifiers we want to keep
|
||||
const filteredSpecifiers = specifiers.filter((specifier) => {
|
||||
const shouldSkip =
|
||||
specifier.startsWith('---') ||
|
||||
specifier.startsWith('npm:') ||
|
||||
specifier.startsWith('static:') ||
|
||||
specifier.startsWith('vfs:') ||
|
||||
specifier.startsWith('https:') ||
|
||||
specifier.startsWith('jsr:')
|
||||
|
||||
if (shouldSkip) {
|
||||
console.log('Skipping specifier:', specifier)
|
||||
} else {
|
||||
console.log('Keeping specifier:', specifier)
|
||||
}
|
||||
|
||||
return !shouldSkip
|
||||
})
|
||||
|
||||
console.log('Filtered specifiers count:', filteredSpecifiers.length)
|
||||
console.log('Filtered specifiers:', JSON.stringify(filteredSpecifiers))
|
||||
|
||||
// Then process each one
|
||||
for (const specifier of filteredSpecifiers) {
|
||||
try {
|
||||
// Try to get the module source
|
||||
const moduleSource = await parser.getModuleSource(specifier)
|
||||
|
||||
// Get the file path
|
||||
const filePath = url2path(specifier)
|
||||
|
||||
// Create a file object
|
||||
const file = new File([moduleSource], filePath)
|
||||
|
||||
files.push(file)
|
||||
} catch (error) {
|
||||
console.error('Error processing specifier:', specifier, error)
|
||||
}
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
@@ -24,7 +24,6 @@
|
||||
"dependencies": {
|
||||
"@ai-sdk/openai": "^0.0.72",
|
||||
"@dagrejs/dagre": "^1.0.4",
|
||||
"@deno/eszip": "0.83.0",
|
||||
"@dnd-kit/core": "^6.1.0",
|
||||
"@dnd-kit/sortable": "^8.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { parseEszip } from 'lib/eszip-parser'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { method } = req
|
||||
|
||||
switch (method) {
|
||||
case 'POST':
|
||||
return handlePost(req, res)
|
||||
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(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { projectRef, slug } = req.body || {}
|
||||
|
||||
if (!projectRef) {
|
||||
return res.status(400).json({ error: 'projectRef is required' })
|
||||
}
|
||||
if (!slug) {
|
||||
return res.status(400).json({ error: 'slug is required' })
|
||||
}
|
||||
|
||||
// Get authorization token from the request
|
||||
const authToken = req.headers.authorization
|
||||
|
||||
if (!authToken) {
|
||||
return res.status(401).json({ error: 'No authorization token was found' })
|
||||
}
|
||||
|
||||
// Fetch the eszip data
|
||||
const headers = new Headers()
|
||||
headers.set('Accept', 'application/octet-stream')
|
||||
headers.set('Authorization', typeof authToken === 'string' ? authToken : authToken[0])
|
||||
|
||||
// Forward other important headers
|
||||
if (req.headers.cookie) {
|
||||
headers.set('Cookie', req.headers.cookie)
|
||||
}
|
||||
|
||||
const baseUrl = API_URL?.replace('/platform', '')
|
||||
const url = `${baseUrl}/v1/projects/${projectRef}/functions/${slug}/body`
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
credentials: 'include',
|
||||
referrerPolicy: 'no-referrer-when-downgrade',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
return res.status(response.status).json(error)
|
||||
}
|
||||
|
||||
// Verify content type is binary/eszip
|
||||
const contentType = response.headers.get('content-type')
|
||||
if (!contentType || !contentType.includes('application/octet-stream')) {
|
||||
return res.status(400).json({
|
||||
error:
|
||||
'Invalid response: Expected eszip file but received ' + (contentType || 'unknown format'),
|
||||
})
|
||||
}
|
||||
|
||||
// Get the eszip data as ArrayBuffer
|
||||
const arrayBuffer = await response.arrayBuffer()
|
||||
|
||||
if (arrayBuffer.byteLength === 0) {
|
||||
return res.status(400).json({ error: 'Invalid eszip: File is empty' })
|
||||
}
|
||||
|
||||
const uint8Array = new Uint8Array(arrayBuffer)
|
||||
|
||||
// Parse the eszip file using our utility
|
||||
const files = await parseEszip(uint8Array)
|
||||
|
||||
return res.status(200).json({
|
||||
files,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error processing edge function body:', error)
|
||||
return res.status(500).json({ error: 'Internal server error' })
|
||||
}
|
||||
}
|
||||
Generated
+3
-21
@@ -547,9 +547,6 @@ importers:
|
||||
'@dagrejs/dagre':
|
||||
specifier: ^1.0.4
|
||||
version: 1.0.4
|
||||
'@deno/eszip':
|
||||
specifier: 0.83.0
|
||||
version: 0.83.0
|
||||
'@dnd-kit/core':
|
||||
specifier: ^6.1.0
|
||||
version: 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@@ -2701,15 +2698,9 @@ packages:
|
||||
resolution: {integrity: sha512-calbMa7Gcyo+/t23XBaqQqon8LlgE9regey4UVoikoenKBXvUnCUL3s9RP6USCxttfr0XWVICtYUuKMdehKqMw==}
|
||||
engines: {node: '>17.0.0'}
|
||||
|
||||
'@deno/eszip@0.83.0':
|
||||
resolution: {integrity: sha512-gTKYMQ+uv20IUJuEBYkjovMPflFjX7caJ8cwA/sZVqic0L/PFP2gZMFt/GiCHc8eVejhlJLGxg0J4qehDq/f2A==}
|
||||
|
||||
'@deno/shim-deno-test@0.5.0':
|
||||
resolution: {integrity: sha512-4nMhecpGlPi0cSzT67L+Tm+GOJqvuk8gqHBziqcUQOarnuIax1z96/gJHCSIz2Z0zhxE6Rzwb3IZXPtFh51j+w==}
|
||||
|
||||
'@deno/shim-deno@0.18.2':
|
||||
resolution: {integrity: sha512-oQ0CVmOio63wlhwQF75zA4ioolPvOwAoK0yuzcS5bDC1JUvH3y1GS8xPh8EOpcoDQRU4FTG8OQfxhpR+c6DrzA==}
|
||||
|
||||
'@deno/shim-deno@0.19.2':
|
||||
resolution: {integrity: sha512-q3VTHl44ad8T2Tw2SpeAvghdGOjlnLPDNO2cpOxwMrBE/PVas6geWpbpIgrM+czOCH0yejp0yi8OaTuB+NU40Q==}
|
||||
|
||||
@@ -8675,6 +8666,7 @@ packages:
|
||||
resolution: {integrity: sha512-t0q23FIpvHDTtnORW+bDJziGsal5uh9RJTJ1fyH8drd4lICOoXhJ5pLMUZ5C0VQei6dNmwTzzoTRgMkO9JgHEQ==}
|
||||
peerDependencies:
|
||||
eslint: '>= 5'
|
||||
bundledDependencies: []
|
||||
|
||||
eslint-plugin-import@2.29.1:
|
||||
resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==}
|
||||
@@ -15739,18 +15731,8 @@ snapshots:
|
||||
|
||||
'@dagrejs/graphlib@2.1.13': {}
|
||||
|
||||
'@deno/eszip@0.83.0':
|
||||
dependencies:
|
||||
'@deno/shim-deno': 0.18.2
|
||||
undici: 6.21.2
|
||||
|
||||
'@deno/shim-deno-test@0.5.0': {}
|
||||
|
||||
'@deno/shim-deno@0.18.2':
|
||||
dependencies:
|
||||
'@deno/shim-deno-test': 0.5.0
|
||||
which: 4.0.0
|
||||
|
||||
'@deno/shim-deno@0.19.2':
|
||||
dependencies:
|
||||
'@deno/shim-deno-test': 0.5.0
|
||||
@@ -23814,7 +23796,7 @@ snapshots:
|
||||
|
||||
has-tostringtag@1.0.2:
|
||||
dependencies:
|
||||
has-symbols: 1.0.3
|
||||
has-symbols: 1.1.0
|
||||
|
||||
has-unicode@2.0.1: {}
|
||||
|
||||
@@ -27145,7 +27127,7 @@ snapshots:
|
||||
|
||||
periscopic@3.1.0:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.5
|
||||
'@types/estree': 1.0.7
|
||||
estree-walker: 3.0.3
|
||||
is-reference: 3.0.3
|
||||
|
||||
|
||||
Reference in new issue
Block a user