feat: use parsed function responses from mgmt-api

This commit is contained in:
Lakshan Perera committed 2025-04-29 15:59:08 +07:00
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)
-131
View File
@@ -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
}
-1
View File
@@ -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' })
}
}
+3 -21
View File
@@ -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