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 ec8d2bfc867..8cf84698362 100644 --- a/apps/studio/data/edge-functions/edge-function-body-query.ts +++ b/apps/studio/data/edge-functions/edge-function-body-query.ts @@ -7,6 +7,7 @@ import { edgeFunctionsKeys } from './keys' export type EdgeFunctionBodyVariables = { projectRef?: string slug?: string + entrypoint?: string } export type EdgeFunctionFile = { @@ -19,11 +20,12 @@ export type EdgeFunctionBodyResponse = { } export async function getEdgeFunctionBody( - { projectRef, slug }: EdgeFunctionBodyVariables, + { projectRef, slug, entrypoint }: EdgeFunctionBodyVariables, signal?: AbortSignal ) { if (!projectRef) throw new Error('projectRef is required') if (!slug) throw new Error('slug is required') + if (!entrypoint) throw new Error('entrypoint is required') try { // Get authorization headers @@ -34,7 +36,7 @@ export async function getEdgeFunctionBody( // 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 }), + body: JSON.stringify({ projectRef, slug, entrypoint }), headers, credentials: 'include', signal, @@ -59,7 +61,7 @@ export type EdgeFunctionBodyData = Awaited( - { projectRef, slug }: EdgeFunctionBodyVariables, + { projectRef, slug, entrypoint }: EdgeFunctionBodyVariables, { enabled = true, ...options @@ -67,7 +69,7 @@ export const useEdgeFunctionBodyQuery = ( ) => useQuery( edgeFunctionsKeys.body(projectRef, slug), - ({ signal }) => getEdgeFunctionBody({ projectRef, slug }, signal), + ({ signal }) => getEdgeFunctionBody({ projectRef, slug, entrypoint }, signal), { enabled: IS_PLATFORM && enabled && typeof projectRef !== 'undefined' && typeof slug !== 'undefined', diff --git a/apps/studio/lib/eszip-parser.ts b/apps/studio/lib/eszip-parser.ts index e6a5e155564..bf84bc38c6a 100644 --- a/apps/studio/lib/eszip-parser.ts +++ b/apps/studio/lib/eszip-parser.ts @@ -4,28 +4,7 @@ import path from 'path' function url2path(url: string) { try { // Parse the URL - const parsedUrl = new URL(url) - // Get the pathname and split it - const pathParts = parsedUrl.pathname.split('/').filter(Boolean) - - // For user function paths like "user_fn_ukekbxwcwofeosgskskm_66727868-6415-49d7-b518-2a057c8377cb_1/data.json" - // Just return the filename (data.json) - if (pathParts.length > 0) { - // Check if it's a user function path with UUID - if (pathParts[0].startsWith('user_fn_')) { - // Get just the filename (last part) - return pathParts[pathParts.length - 1] - } - - // If it starts with 'tmp/' or similar, remove that prefix - if (pathParts[0] === 'tmp' || pathParts[0] === 'deno') { - // Get just the filename (last part) - return pathParts[pathParts.length - 1] - } - } - - // Otherwise, return the full path without leading/trailing slashes - return pathParts.join('/') + return new URL(url).pathname } catch (error) { // If URL parsing fails, fallback to extracting just the filename console.warn('Failed to parse URL:', url) diff --git a/apps/studio/pages/api/edge-functions/body.ts b/apps/studio/pages/api/edge-functions/body.ts index 317bddc189f..39568413321 100644 --- a/apps/studio/pages/api/edge-functions/body.ts +++ b/apps/studio/pages/api/edge-functions/body.ts @@ -1,6 +1,7 @@ import { API_URL } from 'lib/constants' import { parseEszip } from 'lib/eszip-parser' import { NextApiRequest, NextApiResponse } from 'next' +import path from 'path' export default async function handler(req: NextApiRequest, res: NextApiResponse) { const { method } = req @@ -10,7 +11,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) return handlePost(req, res) default: return new Response( - JSON.stringify({ data: null, error: { message: `Method ${method} Not Allowed` } }), + JSON.stringify({ + data: null, + error: { message: `Method ${method} Not Allowed` }, + }), { status: 405, headers: { 'Content-Type': 'application/json', Allow: 'POST' }, @@ -19,9 +23,22 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) } } +function getBasePath(entrypoint: string | undefined): string { + if (!entrypoint) { + return '/' + } + + try { + return path.dirname(new URL(entrypoint).pathname) + } catch (e) { + console.error('failed to parse entrypoint', entrypoint) + return '/' + } +} + async function handlePost(req: NextApiRequest, res: NextApiResponse) { try { - const { projectRef, slug } = req.body || {} + const { projectRef, slug, entrypoint } = req.body || {} if (!projectRef) { return res.status(400).json({ error: 'projectRef is required' }) @@ -29,12 +46,17 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse) { if (!slug) { return res.status(400).json({ error: 'slug is required' }) } + if (!entrypoint) { + return res.status(400).json({ error: 'entrypoint 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' }) + return res.status(401).json({ + error: 'No authorization token was found', + }) } // Fetch the eszip data @@ -81,7 +103,17 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse) { const uint8Array = new Uint8Array(arrayBuffer) // Parse the eszip file using our utility - const files = await parseEszip(uint8Array) + let files = await parseEszip(uint8Array) + + // ignnore empty files + files = files.filter((file: { name: string; content: string }) => !!file.content.length) + + // set file paths relative to entrypoint + const base_path = getBasePath(entrypoint) + files = files.map((file: { name: string; content: string }) => { + file.name = path.relative(base_path, file.name) + return file + }) return res.status(200).json({ files, diff --git a/apps/studio/pages/project/[ref]/functions/[functionSlug]/code.tsx b/apps/studio/pages/project/[ref]/functions/[functionSlug]/code.tsx index 56bdaf778c8..c3a055c775b 100644 --- a/apps/studio/pages/project/[ref]/functions/[functionSlug]/code.tsx +++ b/apps/studio/pages/project/[ref]/functions/[functionSlug]/code.tsx @@ -34,7 +34,9 @@ const CodePage = () => { } = useEdgeFunctionBodyQuery({ projectRef: ref, slug: functionSlug, + entrypoint: selectedFunction?.entrypoint_path, }) + const [files, setFiles] = useState< { id: number; name: string; content: string; selected?: boolean }[] >([]) @@ -49,12 +51,8 @@ const CodePage = () => { if (isDeploying || !ref || !functionSlug || !selectedFunction || files.length === 0) return try { - const parsedEntrypointPath = - selectedFunction.entrypoint_path?.replace(/\/tmp\/user_fn_[^/]+\//, '') || 'index.ts' - const existingFile = files.find((file) => file.name === parsedEntrypointPath) || - files.find((file) => file.name.endsWith('.ts') || file.name.endsWith('.js')) || { - name: 'index.ts', - } + const newEntrypointPath = selectedFunction.entrypoint_path?.split('/').pop() + const newImportMapPath = selectedFunction.import_map_path?.split('/').pop() await deployFunction({ projectRef: ref, @@ -62,8 +60,8 @@ const CodePage = () => { metadata: { name: selectedFunction.name, verify_jwt: selectedFunction.verify_jwt, - entrypoint_path: existingFile.name, - import_map_path: selectedFunction.import_map_path, + entrypoint_path: newEntrypointPath || 'index.ts', + import_map_path: newImportMapPath || '', }, files: files.map(({ name, content }) => ({ name, content })), })