diff --git a/apps/studio/components/interfaces/BranchManagement/EdgeFunctionsDiffPanel.tsx b/apps/studio/components/interfaces/BranchManagement/EdgeFunctionsDiffPanel.tsx index ad99d760f6cce..d91ae01541efc 100644 --- a/apps/studio/components/interfaces/BranchManagement/EdgeFunctionsDiffPanel.tsx +++ b/apps/studio/components/interfaces/BranchManagement/EdgeFunctionsDiffPanel.tsx @@ -13,6 +13,11 @@ import { EMPTY_ARR } from 'lib/void' import { basename } from 'path' import { Card, CardContent, CardHeader, CardTitle, cn, Skeleton } from 'ui' +const EMPTY_FUNCTION_BODY: EdgeFunctionBodyData = { + version: 0, + files: EMPTY_ARR, +} + interface EdgeFunctionsDiffPanelProps { diffResults: EdgeFunctionsDiffResult currentBranchRef?: string @@ -81,11 +86,11 @@ const FunctionDiff = ({ } }, [allFileKeys, activeFileKey]) - const currentFile = currentBody.find( - (f: EdgeFunctionBodyData[number]) => fileKey(f.name) === activeFileKey + const currentFile = currentBody.files.find( + (f: EdgeFunctionBodyData['files'][number]) => fileKey(f.name) === activeFileKey ) - const mainFile = mainBody.find( - (f: EdgeFunctionBodyData[number]) => fileKey(f.name) === activeFileKey + const mainFile = mainBody.files.find( + (f: EdgeFunctionBodyData['files'][number]) => fileKey(f.name) === activeFileKey ) const language = useMemo(() => { @@ -189,7 +194,7 @@ const EdgeFunctionsDiffPanel = ({ key={slug} functionSlug={slug} currentBody={diffResults.addedBodiesMap[slug]!} - mainBody={EMPTY_ARR} + mainBody={EMPTY_FUNCTION_BODY} currentBranchRef={currentBranchRef} fileInfos={diffResults.functionFileInfo[slug] || EMPTY_ARR} /> diff --git a/apps/studio/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout.tsx b/apps/studio/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout.tsx index d42be8ca5ae36..ae035f651381e 100644 --- a/apps/studio/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout.tsx +++ b/apps/studio/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout.tsx @@ -56,22 +56,23 @@ const EdgeFunctionDetailsLayout = ({ isError, } = useEdgeFunctionQuery({ projectRef: ref, slug: functionSlug }) - const { data: functionFiles = [], error: filesError } = useEdgeFunctionBodyQuery( - { - projectRef: ref, - slug: functionSlug, - }, - { - retry: false, - retryOnMount: true, - refetchOnWindowFocus: false, - staleTime: Infinity, - refetchOnMount: false, - refetchOnReconnect: false, - refetchInterval: false, - refetchIntervalInBackground: false, - } - ) + const { data: functionBody = { version: 0, files: [] }, error: filesError } = + useEdgeFunctionBodyQuery( + { + projectRef: ref, + slug: functionSlug, + }, + { + retry: false, + retryOnMount: true, + refetchOnWindowFocus: false, + staleTime: Infinity, + refetchOnMount: false, + refetchOnReconnect: false, + refetchInterval: false, + refetchIntervalInBackground: false, + } + ) const name = selectedFunction?.name || '' @@ -112,7 +113,7 @@ const EdgeFunctionDetailsLayout = ({ const zipFileWriter = new BlobWriter('application/zip') const zipWriter = new ZipWriter(zipFileWriter, { bufferedWrite: true }) - functionFiles.forEach((file) => { + functionBody.files.forEach((file) => { const nameSections = file.name.split('/') const slugIndex = nameSections.indexOf(functionSlug ?? '') const fileName = nameSections.slice(slugIndex + 1).join('/') diff --git a/apps/studio/components/layouts/Tabs/Tabs.utils.ts b/apps/studio/components/layouts/Tabs/Tabs.utils.ts index f67aea0045675..852baa2187d0e 100644 --- a/apps/studio/components/layouts/Tabs/Tabs.utils.ts +++ b/apps/studio/components/layouts/Tabs/Tabs.utils.ts @@ -50,7 +50,7 @@ export function useTableEditorTabsCleanUp() { // e.g Using the SQL editor to rename the entity const openTabs = openTabsRef.current .map((id) => tabMapRef.current[id]) - .filter((tab) => editorEntityTypes['table']?.includes(tab.type)) + .filter((tab) => !!tab && editorEntityTypes['table']?.includes(tab.type)) openTabs.forEach((tab) => { const entity = entities?.find((x) => tab.metadata?.tableId === x.id) @@ -97,7 +97,7 @@ export function useSqlEditorTabsCleanup() { // e.g for a shared snippet, the owner could've updated the name of the snippet const openSqlTabs = openTabsRef.current .map((id) => tabMapRef.current[id]) - .filter((tab) => editorEntityTypes['sql']?.includes(tab.type)) + .filter((tab) => !!tab && editorEntityTypes['sql']?.includes(tab.type)) openSqlTabs.forEach((tab) => { const snippet = snippets?.find((x) => tab.metadata?.sqlId === x.id) 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 d604b96395f60..ddd91121faf59 100644 --- a/apps/studio/data/edge-functions/edge-function-body-query.ts +++ b/apps/studio/data/edge-functions/edge-function-body-query.ts @@ -15,6 +15,7 @@ export type EdgeFunctionFile = { } export type EdgeFunctionBodyResponse = { + version: number files: EdgeFunctionFile[] } @@ -51,11 +52,14 @@ export async function getEdgeFunctionBody( ) } - const { files } = await parseResponse.json() - return files as EdgeFunctionFile[] + const response = (await parseResponse.json()) as EdgeFunctionBodyResponse + return response } catch (error) { handleError(error) - return [] + return { + version: 0, + files: [], + } as EdgeFunctionBodyResponse } } diff --git a/apps/studio/hooks/branches/useEdgeFunctionsDiff.ts b/apps/studio/hooks/branches/useEdgeFunctionsDiff.ts index 4319a90f7cb2f..7f8629b974dc7 100644 --- a/apps/studio/hooks/branches/useEdgeFunctionsDiff.ts +++ b/apps/studio/hooks/branches/useEdgeFunctionsDiff.ts @@ -201,13 +201,15 @@ export const useEdgeFunctionsDiff = ({ const mainBody = mainBodiesMap[slug] if (!currentBody || !mainBody) return - const allFileKeys = new Set([...currentBody, ...mainBody].map((f) => fileKey(f.name))) + const allFileKeys = new Set( + [...currentBody.files, ...mainBody.files].map((f) => fileKey(f.name)) + ) const fileInfos: FileInfo[] = [] let hasModifications = false for (const key of allFileKeys) { - const currentFile = currentBody.find((f) => fileKey(f.name) === key) - const mainFile = mainBody.find((f) => fileKey(f.name) === key) + const currentFile = currentBody.files.find((f) => fileKey(f.name) === key) + const mainFile = mainBody.files.find((f) => fileKey(f.name) === key) let status: FileStatus = 'unchanged' @@ -236,7 +238,7 @@ export const useEdgeFunctionsDiff = ({ addedSlugs.forEach((slug) => { const body = addedBodiesMap[slug] if (body) { - functionFileInfo[slug] = body.map((file) => ({ + functionFileInfo[slug] = body.files.map((file) => ({ key: fileKey(file.name), status: 'added' as FileStatus, })) @@ -247,7 +249,7 @@ export const useEdgeFunctionsDiff = ({ removedSlugs.forEach((slug) => { const body = removedBodiesMap[slug] if (body) { - functionFileInfo[slug] = body.map((file) => ({ + functionFileInfo[slug] = body.files.map((file) => ({ key: fileKey(file.name), status: 'removed' as FileStatus, })) diff --git a/apps/studio/instrumentation-client.ts b/apps/studio/instrumentation-client.ts index e1543a0e1507f..bafe2feee5980 100644 --- a/apps/studio/instrumentation-client.ts +++ b/apps/studio/instrumentation-client.ts @@ -64,7 +64,7 @@ Sentry.init({ `Failed to construct 'URL': Invalid URL` ) // [Joshen] Similar behaviour for this error from SessionTimeoutModal to control the quota usage - const isSessionTimeoutEvent = (hint.originalException as any)?.message.includes( + const isSessionTimeoutEvent = (hint.originalException as any)?.message?.includes( 'Session error detected' ) diff --git a/apps/studio/lib/eszip-parser.test.ts b/apps/studio/lib/eszip-parser.test.ts index 4577549e85cec..ec38531a19d73 100644 --- a/apps/studio/lib/eszip-parser.test.ts +++ b/apps/studio/lib/eszip-parser.test.ts @@ -62,6 +62,7 @@ describe('eszip-parser', () => { mockParser.parseBytes.mockResolvedValue(mockSpecifiers) mockParser.load.mockResolvedValue(undefined) mockParser.getModuleSource + .mockResolvedValueOnce(null) .mockResolvedValueOnce(mockModuleSource1) .mockResolvedValueOnce(mockModuleSource2) @@ -70,12 +71,13 @@ describe('eszip-parser', () => { expect(Parser.createInstance).toHaveBeenCalledTimes(1) expect(mockParser.parseBytes).toHaveBeenCalledWith(mockBytes) expect(mockParser.load).toHaveBeenCalled() - expect(result).toHaveLength(2) - expect(result[0]).toEqual({ + expect(result.version).toEqual(0) + expect(result.files).toHaveLength(2) + expect(result.files[0]).toEqual({ name: 'file1.ts', content: mockModuleSource1, }) - expect(result[1]).toEqual({ + expect(result.files[1]).toEqual({ name: 'file2.ts', content: mockModuleSource2, }) @@ -111,9 +113,10 @@ describe('eszip-parser', () => { const result = await parseEszip(mockBytes) // Only file1.ts and file2.ts should be included - expect(result).toHaveLength(2) - expect(result[0].name).toBe('file1.ts') - expect(result[1].name).toBe('file2.ts') + expect(result.version).toEqual(0) + expect(result.files).toHaveLength(2) + expect(result.files[0].name).toBe('file1.ts') + expect(result.files[1].name).toBe('file2.ts') }) }) }) diff --git a/apps/studio/lib/eszip-parser.ts b/apps/studio/lib/eszip-parser.ts index bf84bc38c6afb..7aa5428e7478e 100644 --- a/apps/studio/lib/eszip-parser.ts +++ b/apps/studio/lib/eszip-parser.ts @@ -63,8 +63,14 @@ export async function parseEszip(bytes: Uint8Array) { throw loadError } + // Extract version + let version = parseInt(await parser.getModuleSource('---SUPABASE-ESZIP-VERSION-ESZIP---')) + if (isNaN(version)) { + version = 0 + } + // Extract files from the eszip - const files = await extractEszip(parser, specifiers) + const files = await extractEszip(parser, specifiers, version >= 2) // Convert files to the expected format const responseFiles = await Promise.all( @@ -77,14 +83,17 @@ export async function parseEszip(bytes: Uint8Array) { }) ) - return responseFiles + return { + version, + files: responseFiles, + } } catch (error) { console.error('Error in parseEszip:', error) throw error } } -async function extractEszip(parser: any, specifiers: string[]) { +async function extractEszip(parser: any, specifiers: string[], isDeno2: boolean) { const files = [] // First, filter out the specifiers we want to keep @@ -114,9 +123,13 @@ async function extractEszip(parser: any, specifiers: string[]) { try { // Try to get the module source const moduleSource = await parser.getModuleSource(specifier) + let qualifiedSpecifier = specifier // Get the file path - const filePath = url2path(specifier) + if (isDeno2 && !specifier.startsWith('file://')) { + qualifiedSpecifier = `file://${specifier}` + } + const filePath = url2path(qualifiedSpecifier) // Create a file object const file = new File([moduleSource], filePath) diff --git a/apps/studio/pages/api/edge-functions/body.ts b/apps/studio/pages/api/edge-functions/body.ts index 317bddc189f9b..ed220a07144ed 100644 --- a/apps/studio/pages/api/edge-functions/body.ts +++ b/apps/studio/pages/api/edge-functions/body.ts @@ -81,11 +81,9 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse) { const uint8Array = new Uint8Array(arrayBuffer) // Parse the eszip file using our utility - const files = await parseEszip(uint8Array) + const parsed = await parseEszip(uint8Array) - return res.status(200).json({ - files, - }) + return res.status(200).json(parsed) } catch (error) { console.error('Error processing edge function body:', error) return res.status(500).json({ error: 'Internal server error' }) diff --git a/apps/studio/pages/project/[ref]/functions/[functionSlug]/code.tsx b/apps/studio/pages/project/[ref]/functions/[functionSlug]/code.tsx index be79cc4262440..21ab897ec4445 100644 --- a/apps/studio/pages/project/[ref]/functions/[functionSlug]/code.tsx +++ b/apps/studio/pages/project/[ref]/functions/[functionSlug]/code.tsx @@ -35,7 +35,7 @@ const CodePage = () => { const { data: selectedFunction } = useEdgeFunctionQuery({ projectRef: ref, slug: functionSlug }) const { - data: functionFiles, + data: functionBody, isLoading: isLoadingFiles, isError: isErrorLoadingFiles, isSuccess: isSuccessLoadingFiles, @@ -123,15 +123,29 @@ const CodePage = () => { } } - function getBasePath(entrypoint: string | undefined): string { + function getBasePath( + entrypoint: string | undefined, + fileNames: string[], + version: number + ): string { if (!entrypoint) { return '/' } + let qualifiedEntrypoint = entrypoint + + if (version >= 2) { + const candidate = fileNames.find((name) => entrypoint.endsWith(name)) + if (candidate) { + qualifiedEntrypoint = `file://${candidate}` + } else { + qualifiedEntrypoint = entrypoint + } + } try { - return dirname(new URL(entrypoint).pathname) + return dirname(new URL(qualifiedEntrypoint).pathname) } catch (e) { - console.error('Failed to parse entrypoint', entrypoint) + console.error('Failed to parse entrypoint', qualifiedEntrypoint) return '/' } } @@ -155,9 +169,13 @@ const CodePage = () => { useEffect(() => { // Set files from API response when available - if (selectedFunction?.entrypoint_path && functionFiles) { - const base_path = getBasePath(selectedFunction?.entrypoint_path) - const filesWithRelPath = functionFiles + if (selectedFunction?.entrypoint_path && functionBody) { + const base_path = getBasePath( + selectedFunction?.entrypoint_path, + functionBody.files.map((file) => file.name), + functionBody.version + ) + const filesWithRelPath = functionBody.files // ignore empty files .filter((file: { name: string; content: string }) => !!file.content.length) // set file paths relative to entrypoint @@ -192,7 +210,7 @@ const CodePage = () => { }) } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [functionFiles]) + }, [functionBody]) return (