diff --git a/apps/studio/components/interfaces/Database/Hooks/EditHookPanel.tsx b/apps/studio/components/interfaces/Database/Hooks/EditHookPanel.tsx index 40db2124f0a..aae54a2bf15 100644 --- a/apps/studio/components/interfaces/Database/Hooks/EditHookPanel.tsx +++ b/apps/studio/components/interfaces/Database/Hooks/EditHookPanel.tsx @@ -21,22 +21,11 @@ import { } from '@/data/table-editor/table-editor-query' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { useConfirmOnClose } from '@/hooks/ui/useConfirmOnClose' +import { isEdgeFunctionUrl } from '@/lib/api/edgeFunctions' import { uuidv4 } from '@/lib/helpers' export type HTTPArgument = { id: string; name: string; value: string } -export const isEdgeFunction = ({ - ref, - restUrlTld, - url, -}: { - ref?: string - restUrlTld?: string - url: string -}) => - url.includes(`https://${ref}.functions.supabase.${restUrlTld}/`) || - url.includes(`https://${ref}.supabase.${restUrlTld}/functions/`) - const FORM_ID = 'edit-hook-panel-form' const parseHeaders = (selectedHook?: PGTrigger): HTTPArgument[] => { @@ -143,7 +132,6 @@ export const EditHookPanel = () => { const isSubmitting = isCreating || isUpdating || isLoadingTable const restUrl = project?.restUrl - const restUrlTld = restUrl ? new URL(restUrl).hostname.split('.').pop() : 'co' const form = useForm({ resolver: zodResolver(FormSchema), @@ -152,11 +140,7 @@ export const EditHookPanel = () => { table_id: selectedHook?.table_id?.toString() ?? '', http_url: selectedHook?.function_args?.[0] ?? '', http_method: (selectedHook?.function_args?.[1] as 'GET' | 'POST') ?? 'POST', - function_type: isEdgeFunction({ - ref, - restUrlTld, - url: selectedHook?.function_args?.[0] ?? '', - }) + function_type: isEdgeFunctionUrl(selectedHook?.function_args?.[0] ?? '', ref ?? '', restUrl) ? 'supabase_function' : 'http_request', timeout_ms: Number(selectedHook?.function_args?.[4] ?? 5000), @@ -188,11 +172,7 @@ export const EditHookPanel = () => { table_id: selectedHook?.table_id?.toString() ?? '', http_url: selectedHook?.function_args?.[0] ?? '', http_method: (selectedHook?.function_args?.[1] as 'GET' | 'POST') ?? 'POST', - function_type: isEdgeFunction({ - ref, - restUrlTld, - url: selectedHook?.function_args?.[0] ?? '', - }) + function_type: isEdgeFunctionUrl(selectedHook?.function_args?.[0] ?? '', ref ?? '', restUrl) ? 'supabase_function' : 'http_request', timeout_ms: Number(selectedHook?.function_args?.[4] ?? 5000), @@ -201,7 +181,7 @@ export const EditHookPanel = () => { httpParameters: parseParameters(selectedHook), }) } - }, [visible, selectedHook, ref, restUrlTld, form]) + }, [visible, selectedHook, ref, restUrl, form]) const queryClient = useQueryClient() const onSubmit: SubmitHandler = async (values) => { diff --git a/apps/studio/components/interfaces/Database/Hooks/FormContents.tsx b/apps/studio/components/interfaces/Database/Hooks/FormContents.tsx index 509482267ba..84fdc1d909e 100644 --- a/apps/studio/components/interfaces/Database/Hooks/FormContents.tsx +++ b/apps/studio/components/interfaces/Database/Hooks/FormContents.tsx @@ -22,7 +22,6 @@ import { } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' -import { isEdgeFunction } from './EditHookPanel' import { WebhookFormValues } from './EditHookPanel.constants' import { AVAILABLE_WEBHOOK_TYPES, HOOK_EVENTS } from './Hooks.constants' import { HTTPHeaders } from './HTTPHeaders' @@ -38,6 +37,7 @@ import { useEdgeFunctionsQuery } from '@/data/edge-functions/edge-functions-quer import { useTableNamesQuery } from '@/data/tables/table-names-query' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' +import { buildDatabaseEdgeFunctionUrl, isEdgeFunctionUrl } from '@/lib/api/edgeFunctions' import { uuidv4 } from '@/lib/helpers' export interface FormContentsProps { @@ -50,7 +50,6 @@ export const FormContents = ({ form, selectedHook }: FormContentsProps) => { const { data: project } = useSelectedProjectQuery() const restUrl = project?.restUrl - const restUrlTld = restUrl ? new URL(restUrl).hostname.split('.').pop() : 'co' const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.SECRETS_READ, '*') const { data: keys = [] } = useAPIKeysQuery( @@ -75,7 +74,7 @@ export const FormContents = ({ form, selectedHook }: FormContentsProps) => { useEffect(() => { if (!isSuccessEdgeFunctions) return - const isEdgeFunctionSelected = isEdgeFunction({ ref, restUrlTld, url: httpUrl }) + const isEdgeFunctionSelected = isEdgeFunctionUrl(httpUrl, ref ?? '', restUrl) if (httpUrl && isEdgeFunctionSelected) { const fnSlug = httpUrl.split('/').at(-1) @@ -98,8 +97,16 @@ export const FormContents = ({ form, selectedHook }: FormContentsProps) => { form.setValue('httpHeaders', updatedHttpHeaders) } } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [httpUrl, isSuccessEdgeFunctions]) + }, [ + form, + functions, + httpHeaders, + httpUrl, + isSuccessEdgeFunctions, + legacyServiceRole, + ref, + restUrl, + ]) return (
@@ -235,9 +242,13 @@ export const FormContents = ({ form, selectedHook }: FormContentsProps) => { } else if (functionType === 'supabase_function') { // Default to first edge function in the list const fnSlug = functions[0]?.slug - const defaultFunctionUrl = `https://${ref}.supabase.${restUrlTld}/functions/v1/${fnSlug}` + const defaultFunctionUrl = buildDatabaseEdgeFunctionUrl( + fnSlug ?? '', + ref ?? '', + restUrl + ) const currentUrl = form.getValues('http_url') - if (!isEdgeFunction({ ref, restUrlTld, url: currentUrl })) { + if (!isEdgeFunctionUrl(currentUrl, ref ?? '', restUrl)) { form.setValue('http_url', defaultFunctionUrl, { shouldDirty: false }) } } diff --git a/apps/studio/components/interfaces/Database/Hooks/HTTPRequestConfig.tsx b/apps/studio/components/interfaces/Database/Hooks/HTTPRequestConfig.tsx index a52184fc84f..915ec146060 100644 --- a/apps/studio/components/interfaces/Database/Hooks/HTTPRequestConfig.tsx +++ b/apps/studio/components/interfaces/Database/Hooks/HTTPRequestConfig.tsx @@ -23,6 +23,7 @@ import { } from '@/components/ui/Forms/FormSection' import { useEdgeFunctionsQuery } from '@/data/edge-functions/edge-functions-query' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' +import { buildDatabaseEdgeFunctionUrl } from '@/lib/api/edgeFunctions' interface HTTPRequestConfigProps { form: UseFormReturn @@ -116,8 +117,7 @@ export const HTTPRequestConfig = ({ form }: HTTPRequestConfigProps) => { {edgeFunctions.map((fn) => { const restUrl = selectedProject?.restUrl - const restUrlTld = restUrl ? new URL(restUrl).hostname.split('.').pop() : 'co' - const functionUrl = `https://${ref}.supabase.${restUrlTld}/functions/v1/${fn.slug}` + const functionUrl = buildDatabaseEdgeFunctionUrl(fn.slug, ref ?? '', restUrl) return ( diff --git a/apps/studio/components/interfaces/Database/Hooks/HooksList/HookList.tsx b/apps/studio/components/interfaces/Database/Hooks/HooksList/HookList.tsx index 6b6791393d8..fe681e49113 100644 --- a/apps/studio/components/interfaces/Database/Hooks/HooksList/HookList.tsx +++ b/apps/studio/components/interfaces/Database/Hooks/HooksList/HookList.tsx @@ -19,6 +19,7 @@ import { ButtonTooltip } from '@/components/ui/ButtonTooltip' import { useDatabaseHooksQuery } from '@/data/database-triggers/database-triggers-query' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' +import { isEdgeFunctionUrl } from '@/lib/api/edgeFunctions' import { BASE_PATH } from '@/lib/constants' export interface HookListProps { @@ -38,7 +39,6 @@ export const HookList = ({ schema, filterString }: HookListProps) => { const [, setSelectedHookIdToDelete] = useQueryState('delete', parseAsString.withDefault('')) const restUrl = project?.restUrl - const restUrlTld = restUrl ? new URL(restUrl).hostname.split('.').pop() : 'co' const filteredHooks = (hooks ?? []).filter( (x) => @@ -54,10 +54,8 @@ export const HookList = ({ schema, filterString }: HookListProps) => { return ( <> {filteredHooks.map((x) => { - const isEdgeFunction = (url: string) => - url.includes(`https://${ref}.functions.supabase.${restUrlTld}/`) || - url.includes(`https://${ref}.supabase.${restUrlTld}/functions/`) const [url, method] = x.function_args + const isEdgeFunction = isEdgeFunctionUrl(url, ref ?? '', restUrl) return ( @@ -66,7 +64,7 @@ export const HookList = ({ schema, filterString }: HookListProps) => {
{ layout="fixed" width="20" height="20" - title={isEdgeFunction(url) ? 'Supabase Edge Function' : 'HTTP Request'} + title={isEdgeFunction ? 'Supabase Edge Function' : 'HTTP Request'} />

diff --git a/apps/studio/components/interfaces/Functions/TerminalInstructions.tsx b/apps/studio/components/interfaces/Functions/TerminalInstructions.tsx index 4a091db1df4..5cd3bd4b9d6 100644 --- a/apps/studio/components/interfaces/Functions/TerminalInstructions.tsx +++ b/apps/studio/components/interfaces/Functions/TerminalInstructions.tsx @@ -37,10 +37,6 @@ export const TerminalInstructions = forwardRef< const apiKey = publishableKey?.api_key ?? anonKey?.api_key ?? '[YOUR ANON KEY]' - // get the .co or .net TLD from the restUrl - const restUrl = `https://${endpoint}` - const restUrlTld = !!endpoint ? new URL(restUrl).hostname.split('.').pop() : 'co' - const commands: Commands[] = [ { command: 'supabase functions new hello-world', @@ -68,7 +64,7 @@ export const TerminalInstructions = forwardRef< comment: 'Deploy your function', }, { - command: `curl -L -X POST 'https://${projectRef}.supabase.${restUrlTld}/functions/v1/hello-world' -H 'Authorization: Bearer ${apiKey}'${anonKey?.type === 'publishable' ? ` -H 'apikey: ${apiKey}'` : ''} --data '{"name":"Functions"}'`, + command: `curl -L -X POST '${functionsEndpoint}/hello-world' -H 'Authorization: Bearer ${apiKey}'${anonKey?.type === 'publishable' ? ` -H 'apikey: ${apiKey}'` : ''} --data '{"name":"Functions"}'`, description: 'Invokes the hello-world function', jsx: () => { return ( diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet/CreateCronJobSheet.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet/CreateCronJobSheet.tsx index 31a2d3e0a91..67335691638 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet/CreateCronJobSheet.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet/CreateCronJobSheet.tsx @@ -122,7 +122,11 @@ export const CreateCronJobSheet = ({ open, selectedCronJob, onClose }: CreateCro 'extensions' ) - const cronJobValues = parseCronJobCommand(selectedCronJob?.command || '', project?.ref!) + const cronJobValues = parseCronJobCommand( + selectedCronJob?.command || '', + project?.ref!, + project?.restUrl + ) const defaultValues = { name: selectedCronJob?.jobname || '', diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobPage.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobPage.tsx index a999b6fc37e..b2831ea5648 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobPage.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobPage.tsx @@ -54,7 +54,7 @@ export const CronJobPage = () => { const { data: edgeFunctions = [] } = useEdgeFunctionsQuery({ projectRef: project?.ref }) // Parse the cron job command to check if it's an edge function - const cronJobValues = parseCronJobCommand(job?.command || '', project?.ref!) + const cronJobValues = parseCronJobCommand(job?.command || '', project?.ref!, project?.restUrl) const edgeFunction = cronJobValues.type === 'edge_function' ? cronJobValues.edgeFunctionName : undefined const edgeFunctionSlug = edgeFunction?.split('/functions/v1/').pop() diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.test.ts b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.test.ts index 93c3b1c7390..478349b8fa1 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.test.ts +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.test.ts @@ -151,6 +151,24 @@ describe('parseCronJobCommand', () => { }) }) + it('should return an edge function config for a self-hosted function URL', () => { + const command = `select net.http_post( url:='http://kong:8000/functions/v1/_', headers:=jsonb_build_object('Authorization', 'Bearer something'), timeout_milliseconds:=5000 );` + expect(parseCronJobCommand(command, 'default', undefined, false)).toMatchObject({ + edgeFunctionName: 'http://kong:8000/functions/v1/_', + type: 'edge_function', + }) + }) + + it('should return an edge function config for a self-hosted public function URL', () => { + const command = `select net.http_post( url:='https://default.supabase.localhost/functions/v1/_', headers:=jsonb_build_object('Authorization', 'Bearer something'), timeout_milliseconds:=5000 );` + expect( + parseCronJobCommand(command, 'default', 'https://default.supabase.localhost/rest/v1/', false) + ).toMatchObject({ + edgeFunctionName: 'https://default.supabase.localhost/functions/v1/_', + type: 'edge_function', + }) + }) + it("should return an HTTP request config when there's a query parameter or hash in the URL (also handles edge function)", () => { const command = `select net.http_post( url:='https://random_project_ref.supabase.co/functions/v1/_?first=1#second=2', headers:=jsonb_build_object('Authorization', 'Bearer something'), timeout_milliseconds:=5000 )` expect(parseCronJobCommand(command, 'random_project_ref')).toStrictEqual({ diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.tsx index 22884d07b76..cc28f6118a5 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.tsx @@ -7,6 +7,8 @@ import { CronJobType } from './CreateCronJobSheet/CreateCronJobSheet.constants' import { CRON_TABLE_COLUMNS, HTTPHeader, secondsPattern } from './CronJobs.constants' import { CronJobTableCell } from './CronJobTableCell' import { CronJob } from '@/data/database-cron-jobs/database-cron-jobs-infinite-query' +import { isEdgeFunctionUrl } from '@/lib/api/edgeFunctions' +import { IS_PLATFORM } from '@/lib/constants' const unescapeSqlLiteral = (value = '', isEscapeString = false) => { const unescaped = value.replaceAll("''", "'") @@ -129,7 +131,12 @@ const DEFAULT_CRONJOB_COMMAND = { httpBody: '', } as const -export const parseCronJobCommand = (originalCommand: string, projectRef: string): CronJobType => { +export const parseCronJobCommand = ( + originalCommand: string, + projectRef: string, + restUrl?: string, + isPlatform = IS_PLATFORM +): CronJobType => { const command = originalCommand.replaceAll('$$', ' ').replaceAll(/\n/g, ' ').trim() if (command.toLocaleLowerCase().match(/^select\s+net\./)) { @@ -181,8 +188,7 @@ export const parseCronJobCommand = (originalCommand: string, projectRef: string) } catch {} if ( - url.includes(`${projectRef}.supabase.`) && - url.includes('/functions/v1/') && + isEdgeFunctionUrl(url, projectRef, restUrl, isPlatform) && searchParams.length === 0 && urlHash.length === 0 ) { diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/DeleteCronJob.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/DeleteCronJob.tsx index ae9a700053f..b74e2f98980 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/DeleteCronJob.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/DeleteCronJob.tsx @@ -32,7 +32,7 @@ export const DeleteCronJob = () => { } = useDatabaseCronJobDeleteMutation({ onSuccess: () => { if (cronJob && project) { - const { type } = parseCronJobCommand(cronJob.command, project.ref) + const { type } = parseCronJobCommand(cronJob.command, project.ref, project.restUrl) track('cron_job_removed', { type }) } toast.success(`Successfully removed cron job`) diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/EdgeFunctionSection.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/EdgeFunctionSection.tsx index 4806d8be0cd..5e22a953f78 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/EdgeFunctionSection.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/EdgeFunctionSection.tsx @@ -37,17 +37,12 @@ import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { CreateCronJobForm } from './CreateCronJobSheet/CreateCronJobSheet.constants' import { useEdgeFunctionsQuery } from '@/data/edge-functions/edge-functions-query' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' +import { buildDatabaseEdgeFunctionUrl } from '@/lib/api/edgeFunctions' interface HTTPRequestFieldsProps { form: UseFormReturn } -const buildFunctionUrl = (slug: string, projectRef: string, restUrl?: string) => { - const restUrlTld = restUrl ? new URL(restUrl).hostname.split('.').pop() : 'co' - const functionUrl = `https://${projectRef}.supabase.${restUrlTld}/functions/v1/${slug}` - return functionUrl -} - export const EdgeFunctionSection = ({ form }: HTTPRequestFieldsProps) => { const { ref } = useParams() const { data: selectedProject } = useSelectedProjectQuery() @@ -62,7 +57,11 @@ export const EdgeFunctionSection = ({ form }: HTTPRequestFieldsProps) => { () => functions?.map((fn) => ({ ...fn, - url: buildFunctionUrl(fn.slug, selectedProject?.ref || '', selectedProject?.restUrl), + url: buildDatabaseEdgeFunctionUrl( + fn.slug, + selectedProject?.ref || '', + selectedProject?.restUrl + ), })) ?? [], [functions, selectedProject] ) diff --git a/apps/studio/components/ui/AIAssistantPanel/EdgeFunctionRenderer.tsx b/apps/studio/components/ui/AIAssistantPanel/EdgeFunctionRenderer.tsx index f62e16320a1..bb5c4079511 100644 --- a/apps/studio/components/ui/AIAssistantPanel/EdgeFunctionRenderer.tsx +++ b/apps/studio/components/ui/AIAssistantPanel/EdgeFunctionRenderer.tsx @@ -40,18 +40,10 @@ export const EdgeFunctionRenderer = ({ const functionUrl = useMemo(() => { const endpoint = settings?.app_config?.endpoint + const protocol = settings?.app_config?.protocol ?? 'https' if (!endpoint || !ref || !functionName) return undefined - - try { - const url = new URL(`https://${endpoint}`) - const restUrlTld = url.hostname.split('.').pop() - return restUrlTld - ? `https://${ref}.supabase.${restUrlTld}/functions/v1/${functionName}` - : undefined - } catch (error) { - return undefined - } - }, [settings?.app_config?.endpoint, ref, functionName]) + return `${protocol}://${endpoint}/functions/v1/${functionName}` + }, [settings?.app_config?.endpoint, settings?.app_config?.protocol, ref, functionName]) const deploymentDetailsUrl = useMemo(() => { if (!ref || !functionName) return undefined diff --git a/apps/studio/lib/api/edgeFunctions.test.ts b/apps/studio/lib/api/edgeFunctions.test.ts index 76ac26dda3d..0d6a43d83ae 100644 --- a/apps/studio/lib/api/edgeFunctions.test.ts +++ b/apps/studio/lib/api/edgeFunctions.test.ts @@ -1,6 +1,94 @@ import { describe, expect, it } from 'vitest' -import { isValidEdgeFunctionURL } from './edgeFunctions' +import { + buildDatabaseEdgeFunctionUrl, + isEdgeFunctionUrl, + isValidEdgeFunctionURL, +} from './edgeFunctions' + +describe('buildDatabaseEdgeFunctionUrl', () => { + it('builds a platform edge function URL', () => { + expect( + buildDatabaseEdgeFunctionUrl( + 'hello-world', + 'uniquetwentychararef', + 'https://uniquetwentychararef.supabase.red/rest/v1/', + true + ) + ).toBe('https://uniquetwentychararef.supabase.red/functions/v1/hello-world') + }) + + it('builds a self-hosted URL reachable from Postgres', () => { + expect( + buildDatabaseEdgeFunctionUrl( + 'hello-world', + 'default', + 'http://localhost:8000/rest/v1/', + false + ) + ).toBe('http://kong:8000/functions/v1/hello-world') + }) +}) + +describe('isEdgeFunctionUrl', () => { + it('matches platform edge function URLs for the current project', () => { + expect( + isEdgeFunctionUrl( + 'https://uniquetwentychararef.supabase.co/functions/v1/hello-world', + 'uniquetwentychararef', + 'https://uniquetwentychararef.supabase.co/rest/v1/', + true + ) + ).toBe(true) + }) + + it('matches self-hosted edge function URLs', () => { + expect( + isEdgeFunctionUrl( + 'http://kong:8000/functions/v1/hello-world', + 'default', + 'http://localhost:8000/rest/v1/', + false + ) + ).toBe(true) + }) + + it.each([ + { + name: 'a project URL without the functions path', + url: 'https://uniquetwentychararef.supabase.co/rest/v1/hello-world', + projectRef: 'uniquetwentychararef', + restUrl: 'https://uniquetwentychararef.supabase.co/rest/v1/', + isPlatform: true, + }, + { + name: "another project's edge function URL", + url: 'https://anotherprojectref000.supabase.co/functions/v1/hello-world', + projectRef: 'uniquetwentychararef', + restUrl: 'https://uniquetwentychararef.supabase.co/rest/v1/', + isPlatform: true, + }, + { + name: 'a lookalike project origin', + url: 'https://uniquetwentychararef.supabase.example.com/functions/v1/hello-world', + projectRef: 'uniquetwentychararef', + restUrl: 'https://uniquetwentychararef.supabase.co/rest/v1/', + isPlatform: true, + }, + { + name: 'a self-hosted non-function URL', + url: 'http://kong:8000/rest/v1/hello-world', + projectRef: 'default', + restUrl: 'http://localhost:8000/rest/v1/', + isPlatform: false, + }, + ])('does not match $name', ({ url, projectRef, restUrl, isPlatform }) => { + expect( + isEdgeFunctionUrl(url, projectRef, restUrl, isPlatform), + `Expected ${url} not to match an edge function URL` + ).toBe(false) + }) +}) describe('isValidEdgeFunctionURL', () => { const validEdgeFunctionUrls = [ diff --git a/apps/studio/lib/api/edgeFunctions.ts b/apps/studio/lib/api/edgeFunctions.ts index 4a1a4817436..89bb5a4a522 100644 --- a/apps/studio/lib/api/edgeFunctions.ts +++ b/apps/studio/lib/api/edgeFunctions.ts @@ -1,4 +1,39 @@ +import { IS_PLATFORM } from '@/lib/constants' + const NIMBUS_PROD_PROJECTS_URL = process.env.NIMBUS_PROD_PROJECTS_URL +// Cron jobs and database hooks run inside Postgres, where Kong is available by this network alias. +const SELF_HOSTED_EDGE_FUNCTIONS_URL = 'http://kong:8000/functions/v1' +const PLATFORM_TLDS = ['co', 'red'] as const + +export const buildDatabaseEdgeFunctionUrl = ( + slug: string, + projectRef: string, + restUrl?: string, + isPlatform = IS_PLATFORM +) => { + if (!isPlatform) return `${SELF_HOSTED_EDGE_FUNCTIONS_URL}/${slug}` + + const projectOrigin = restUrl ? new URL(restUrl).origin : `https://${projectRef}.supabase.co` + return `${projectOrigin}/functions/v1/${slug}` +} + +export const isEdgeFunctionUrl = ( + url: string, + projectRef: string, + restUrl?: string, + isPlatform = IS_PLATFORM +) => { + if (!isPlatform && url.startsWith(`${SELF_HOSTED_EDGE_FUNCTIONS_URL}/`)) return true + + const projectOrigin = restUrl ? new URL(restUrl).origin : undefined + if (projectOrigin && url.startsWith(`${projectOrigin}/functions/v1/`)) return true + + return PLATFORM_TLDS.some( + (tld) => + url.startsWith(`https://${projectRef}.functions.supabase.${tld}/`) || + url.startsWith(`https://${projectRef}.supabase.${tld}/functions/`) + ) +} export const isValidEdgeFunctionURL = (url: string, isPlatform: boolean) => { if (NIMBUS_PROD_PROJECTS_URL !== undefined) {