diff --git a/apps/studio/data/edge-functions/edge-function-body-query.ts b/apps/studio/data/edge-functions/edge-function-body-query.ts index 2086fbaff2a..7bcc74fa4d6 100644 --- a/apps/studio/data/edge-functions/edge-function-body-query.ts +++ b/apps/studio/data/edge-functions/edge-function-body-query.ts @@ -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) { + 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) diff --git a/apps/studio/lib/eszip-parser.ts b/apps/studio/lib/eszip-parser.ts deleted file mode 100644 index bf84bc38c6a..00000000000 --- a/apps/studio/lib/eszip-parser.ts +++ /dev/null @@ -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 | 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 -} diff --git a/apps/studio/package.json b/apps/studio/package.json index 393216104e5..4c9976dcfdd 100644 --- a/apps/studio/package.json +++ b/apps/studio/package.json @@ -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", diff --git a/apps/studio/pages/api/edge-functions/body.ts b/apps/studio/pages/api/edge-functions/body.ts deleted file mode 100644 index 317bddc189f..00000000000 --- a/apps/studio/pages/api/edge-functions/body.ts +++ /dev/null @@ -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' }) - } -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ae1312c5e06..377ea2cebfb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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