Migrate Database Webhook integrations to new integration sUI (#44277)

## Context

Related to marketplace integrations

Shifts Database Webhooks integration to the new Integrations UI. This
one's a bit different from the previous PRs as this involves a full SQL
installation query instead of only just a database extension. So am
tweaking `Integrations.constants` a little.

For context eventually all the integrations will be pulled remotely from
a database, so am still trying to figure out an optimal data structure,
but requirements will be clearer as we build out the UI

RE installing integrations:
- For now, if the integration has a provided SQL installation command,
that'll take precedence
- Else, if the integration has a provided SQL installation query, we'll
use that on the /query endpoint
- Otherwise, if the integration only requires database extensions,
dashboard will generate the queries to install the extensions
- In the case of the former tho, we won't allow users to choose which
schema to install the extension in too

Just ping me if any clarification's required!

## To test
- [ ] Verify that you can install the database webhooks with the new
integration UI
- [ ] Verify that behaviour is status quo without the new integration UI
This commit is contained in:
Joshen Lim
2026-03-30 19:39:49 +08:00
committed by GitHub
parent cca4e52dd0
commit 514b097021
8 changed files with 484 additions and 165 deletions

View File

@@ -13,6 +13,7 @@ export interface IntegrationOverviewTabProps {
actions?: ReactNode
status?: string | ReactNode
alert?: ReactNode
hideRequiredExtensionsSection?: boolean
}
export const IntegrationOverviewTab = ({
@@ -20,6 +21,7 @@ export const IntegrationOverviewTab = ({
alert,
status,
children,
hideRequiredExtensionsSection = false,
}: PropsWithChildren<IntegrationOverviewTabProps>) => {
const { id } = useParams()
const { data: project } = useSelectedProjectQuery()
@@ -56,7 +58,7 @@ export const IntegrationOverviewTab = ({
<Separator />
{dependsOnExtension && (
{dependsOnExtension && !hideRequiredExtensionsSection && (
<div className="px-4 md:px-10 max-w-4xl flex flex-col gap-y-4">
<h4>Required extensions</h4>
<Card>

View File

@@ -26,7 +26,6 @@ import {
SheetSection,
SheetTitle,
SheetTrigger,
SonnerProgress,
Tabs_Shadcn_,
TabsContent_Shadcn_,
TabsList_Shadcn_,
@@ -42,6 +41,7 @@ import { extensionsWithRecommendedSchemas } from '@/components/interfaces/Databa
import { useDatabaseExtensionEnableMutation } from '@/data/database-extensions/database-extension-enable-mutation'
import { useDatabaseExtensionsQuery } from '@/data/database-extensions/database-extensions-query'
import { useSchemasQuery } from '@/data/database/schemas-query'
import { useExecuteSqlMutation } from '@/data/sql/execute-sql-mutation'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { useProtectedSchemas } from '@/hooks/useProtectedSchemas'
import { ResponseError } from '@/types'
@@ -50,14 +50,33 @@ interface InstallIntegrationSheetProps {
integration: IntegrationDefinition
}
/**
* [Joshen] Trying to figure out what the ideal data structure is between local + remote integrations
* So it might be a bit messy for now as we get more context and build out this UI
*
* If the integration provides its own SQL installation command, we'll use that
* Otherwise if the integration provides its own SQL installation query, we'll use that through the query endpoint
* Else if the integration only requires extensions, dashboard will generate the queries and fire through the query endpoint
*/
export const InstallIntegrationSheet = ({ integration }: InstallIntegrationSheetProps) => {
const { requiredExtensions: requiredExtensionNames } = integration
const { data: project } = useSelectedProjectQuery()
const { data: protectedSchemas } = useProtectedSchemas({ excludeSchemas: ['extensions'] })
const [open, setOpen] = useState(false)
const [isInstalling, setIsInstalling] = useState(false)
const {
icon,
name,
installationSql,
installationCommand,
missingExtensionsAlert,
requiredExtensions: requiredExtensionNames,
} = integration
const allowExtensionCustomSchema = !installationSql
const defaultExtensionsSchema = Object.fromEntries(
requiredExtensionNames.map((extName) => [extName, { schema: 'extensions', value: undefined }])
)
@@ -85,66 +104,34 @@ export const InstallIntegrationSheet = ({ integration }: InstallIntegrationSheet
(schema) => !protectedSchemas.some((protectedSchema) => protectedSchema.name === schema.name)
)
const { mutateAsync: executeSql } = useExecuteSqlMutation({ onError: () => {} })
const { mutateAsync: enableExtension } = useDatabaseExtensionEnableMutation({ onError: () => {} })
const enableExtensionsSQL = getEnableExtensionsSQL({
extensions: requiredExtensions,
extensionsSchema,
})
const installationSQLContent = installationSql ?? enableExtensionsSQL
/**
* [Joshen] This will be pretty simple for now, but will expand as we bring over more integrations to the new UI to see what else is needed
*/
const onInstallIntegration = async () => {
if (!project) return console.error('Project is required')
setIsInstalling(true)
const toastId = toast.loading(
<SonnerProgress progress={0} message={`Installing ${integration.name}`} />
)
const toastId = toast.loading(`Installing ${name}`)
try {
if (requiredExtensions.length > 0) {
toast.loading(
<SonnerProgress
progress={0}
message={`Installing ${integration.name}`}
description={`Enabling ${requiredExtensions.length} database extension${requiredExtensions.length > 1 ? 's' : ''}`}
/>,
{ id: toastId }
)
const results = await Promise.allSettled(
requiredExtensions.map((ext) => {
const { name, default_version: version } = ext
const createSchema = extensionsSchema[name].schema === 'custom'
const schema =
name === 'pg_cron'
? 'pg_catalog'
: createSchema
? (extensionsSchema[name].value as string)
: extensionsSchema[name].schema
return enableExtension({
projectRef: project.ref,
connectionString: project.connectionString,
schema,
name,
version,
cascade: true,
createSchema,
})
})
)
const failure = results.find((r) => r.status === 'rejected')
if (!!failure) throw new Error(failure.reason.message)
if (!!installationCommand) {
await installationCommand({ ref: project.ref })
} else if (!!installationSql) {
await installIntegrationViaSQL()
} else {
await installIntegrationExtensions()
}
toast.success(`Successfully installed ${integration.name}`, { id: toastId })
toast.success(`Successfully installed ${name}`, { id: toastId })
setOpen(false)
} catch (error) {
toast.error(`Failed to install ${integration.name}: ${(error as ResponseError).message}`, {
toast.error(`Failed to install ${name}: ${(error as ResponseError).message}`, {
id: toastId,
})
} finally {
@@ -152,6 +139,49 @@ export const InstallIntegrationSheet = ({ integration }: InstallIntegrationSheet
}
}
const installIntegrationViaSQL = async () => {
if (!project) return console.error('Project is required')
if (!installationSql) return console.error('Installation SQL is required')
const { ref: projectRef, connectionString } = project
try {
await executeSql({ projectRef, connectionString, sql: installationSql })
} catch (error) {
throw error
}
}
const installIntegrationExtensions = async () => {
if (!project) return console.error('Project is required')
const { ref: projectRef, connectionString } = project
const results = await Promise.allSettled(
requiredExtensions.map((ext) => {
const { name, default_version: version } = ext
const createSchema = extensionsSchema[name].schema === 'custom'
const schema =
name === 'pg_cron'
? 'pg_catalog'
: createSchema
? (extensionsSchema[name].value as string)
: extensionsSchema[name].schema
return enableExtension({
projectRef,
connectionString,
schema,
name,
version,
cascade: true,
createSchema,
})
})
)
const failure = results.find((r) => r.status === 'rejected')
if (!!failure) throw new Error(failure.reason.message)
}
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
@@ -164,10 +194,10 @@ export const InstallIntegrationSheet = ({ integration }: InstallIntegrationSheet
>
<SheetHeader className="flex items-center gap-x-4">
<div className="shrink-0 w-11 h-11 relative bg-white border rounded-md flex items-center justify-center">
{integration.icon()}
{icon()}
</div>
<div className="flex flex-col">
<SheetTitle>Install {integration.name}</SheetTitle>
<SheetTitle>Install {name}</SheetTitle>
<SheetDescription>Review and configure this integration</SheetDescription>
</div>
</SheetHeader>
@@ -181,7 +211,7 @@ export const InstallIntegrationSheet = ({ integration }: InstallIntegrationSheet
</p>
</div>
{hasMissingExtensions && integration.missingExtensionsAlert}
{hasMissingExtensions && missingExtensionsAlert}
<Card>
<CardContent className="px-0 pt-1.5 pb-0">
@@ -232,9 +262,9 @@ export const InstallIntegrationSheet = ({ integration }: InstallIntegrationSheet
hideCopy
hideLineNumbers
language="pgsql"
value={enableExtensionsSQL}
value={installationSQLContent}
wrapperClassName={cn('[&_pre]:px-4 [&_pre]:py-3')}
className="border-0 rounded-none [&_code]:text-[12px] [&_code]:text-foreground"
className="border-0 rounded-none [&_code]:text-[12px] [&_code]:text-foreground max-h-80"
/>
</TabsContent_Shadcn_>
<TabsContent_Shadcn_ value="edge_functions" className="mt-0">
@@ -245,95 +275,99 @@ export const InstallIntegrationSheet = ({ integration }: InstallIntegrationSheet
</Card>
</SheetSection>
<DialogSectionSeparator />
{allowExtensionCustomSchema && (
<>
<DialogSectionSeparator />
<SheetSection>
<Accordion_Shadcn_ type="single" collapsible>
<AccordionItem_Shadcn_ value="advanced-settings" className="border-none">
<AccordionTrigger_Shadcn_ className="font-normal gap-2 py-0 justify-between text-sm hover:no-underline">
Advanced settings
</AccordionTrigger_Shadcn_>
<AccordionContent_Shadcn_ className="!pb-0 pt-3 [&>div]:flex [&>div]:flex-col [&>div]:gap-y-4">
<p className="text-foreground-light">
Select which schemas to install the database extensions under
</p>
{requiredExtensionNames.map((extName) => {
const extMeta = extensionsSchema[extName as keyof typeof extensionsSchema]
const { schema, value } = extMeta
const recommendedSchema = extensionsWithRecommendedSchemas[extName]
<SheetSection>
<Accordion_Shadcn_ type="single" collapsible>
<AccordionItem_Shadcn_ value="advanced-settings" className="border-none">
<AccordionTrigger_Shadcn_ className="font-normal gap-2 py-0 justify-between text-sm hover:no-underline">
Advanced settings
</AccordionTrigger_Shadcn_>
<AccordionContent_Shadcn_ className="!pb-0 pt-3 [&>div]:flex [&>div]:flex-col [&>div]:gap-y-4">
<p className="text-foreground-light">
Select which schemas to install the database extensions under
</p>
{requiredExtensionNames.map((extName) => {
const extMeta = extensionsSchema[extName as keyof typeof extensionsSchema]
const { schema, value } = extMeta
const recommendedSchema = extensionsWithRecommendedSchemas[extName]
return (
<FormItemLayout
key={extName}
isReactForm={false}
layout="horizontal"
label={extName}
>
<Select_Shadcn_
value={schema}
onValueChange={(schema) =>
setExtensionsSchema((prev) => ({
...prev,
[extName]: {
schema,
value: schema === 'custom' ? extName : undefined,
},
}))
}
>
<SelectTrigger_Shadcn_>
<SelectValue_Shadcn_ placeholder="Select a schema" />
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
<SelectItem_Shadcn_ value="custom">
Create a new schema
</SelectItem_Shadcn_>
<SelectSeparator_Shadcn_ />
{availableSchemas.map((schema) => {
return (
<SelectItem_Shadcn_ key={schema.id} value={schema.name}>
{schema.name}
{schema.name === recommendedSchema ? (
<Badge className="ml-2" variant="success">
Recommended
</Badge>
) : schema.name === 'extensions' ? (
<Badge className="ml-2">Default</Badge>
) : null}
</SelectItem_Shadcn_>
)
})}
</SelectContent_Shadcn_>
</Select_Shadcn_>
{schema === 'custom' && (
return (
<FormItemLayout
key={extName}
isReactForm={false}
className="mt-2"
label="Provide a name for your new schema"
layout="horizontal"
label={extName}
>
<Input
value={value}
onChange={(e) =>
<Select_Shadcn_
value={schema}
onValueChange={(schema) =>
setExtensionsSchema((prev) => ({
...prev,
[extName]: {
schema: prev[extName].schema,
value: e.target.value,
schema,
value: schema === 'custom' ? extName : undefined,
},
}))
}
placeholder="Provide a name for your schema"
/>
>
<SelectTrigger_Shadcn_>
<SelectValue_Shadcn_ placeholder="Select a schema" />
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
<SelectItem_Shadcn_ value="custom">
Create a new schema
</SelectItem_Shadcn_>
<SelectSeparator_Shadcn_ />
{availableSchemas.map((schema) => {
return (
<SelectItem_Shadcn_ key={schema.id} value={schema.name}>
{schema.name}
{schema.name === recommendedSchema ? (
<Badge className="ml-2" variant="success">
Recommended
</Badge>
) : schema.name === 'extensions' ? (
<Badge className="ml-2">Default</Badge>
) : null}
</SelectItem_Shadcn_>
)
})}
</SelectContent_Shadcn_>
</Select_Shadcn_>
{schema === 'custom' && (
<FormItemLayout
isReactForm={false}
className="mt-2"
label="Provide a name for your new schema"
>
<Input
value={value}
onChange={(e) =>
setExtensionsSchema((prev) => ({
...prev,
[extName]: {
schema: prev[extName].schema,
value: e.target.value,
},
}))
}
placeholder="Provide a name for your schema"
/>
</FormItemLayout>
)}
</FormItemLayout>
)}
</FormItemLayout>
)
})}
</AccordionContent_Shadcn_>
</AccordionItem_Shadcn_>
</Accordion_Shadcn_>
</SheetSection>
)
})}
</AccordionContent_Shadcn_>
</AccordionItem_Shadcn_>
</Accordion_Shadcn_>
</SheetSection>
</>
)}
<DialogSectionSeparator />
</div>

View File

@@ -1,3 +1,4 @@
import { getEnableWebhooksSQL } from '@supabase/pg-meta'
import { Clock5, Code2, Layers, Timer, Vault, Webhook } from 'lucide-react'
import dynamic from 'next/dynamic'
import Image from 'next/image'
@@ -8,6 +9,9 @@ import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import { UpgradeDatabaseAlert } from '../Queues/UpgradeDatabaseAlert'
import { WRAPPERS } from '../Wrappers/Wrappers.constants'
import { WrapperMeta } from '../Wrappers/Wrappers.types'
import { enableDatabaseWebhooks } from '@/data/database/hooks-enable-mutation'
import { invalidateSchemasQuery } from '@/data/database/schemas-query'
import { getQueryClient } from '@/data/query-client'
import { BASE_PATH, DOCS_URL } from '@/lib/constants'
export type Navigation = {
@@ -48,6 +52,10 @@ export type IntegrationDefinition = {
pageId: string | undefined
childId: string | undefined
}) => ComponentType<{}> | null
/** SQL query for installing the entire integration */
installationSql?: string
/** Custom command to install the integration */
installationCommand?: (props: { ref: string }) => Promise<void>
} & (
| { type: 'wrapper'; meta: WrapperMeta }
| { type: 'postgres_extension' | 'custom' | 'oauth' | 'template' }
@@ -230,7 +238,7 @@ const SUPABASE_INTEGRATIONS: Array<IntegrationDefinition> = [
'Send real-time data from your database to another system when a table event occurs',
docsUrl: DOCS_URL,
author: authorSupabase,
requiredExtensions: [],
requiredExtensions: ['pg_net'],
navigation: [
{
route: 'overview',
@@ -266,6 +274,12 @@ const SUPABASE_INTEGRATIONS: Array<IntegrationDefinition> = [
}
return null
},
installationSql: getEnableWebhooksSQL(),
installationCommand: async ({ ref }: { ref: string }) => {
const queryClient = getQueryClient()
await enableDatabaseWebhooks({ ref })
await invalidateSchemasQuery(queryClient, ref)
},
},
{
id: 'data_api',

View File

@@ -1,5 +1,5 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import { useFlag, useParams } from 'common'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import NoPermission from 'components/ui/NoPermission'
import { useHooksEnableMutation } from 'data/database/hooks-enable-mutation'
@@ -11,11 +11,14 @@ import { Admonition } from 'ui-patterns'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import { IntegrationOverviewTab } from '../Integration/IntegrationOverviewTab'
import { IntegrationOverviewTabV2 } from '../Integration/IntegrationOverviewTabV2'
export const WebhooksOverviewTab = () => {
const { ref: projectRef } = useParams()
const { data: project } = useSelectedProjectQuery()
const isMarketplaceEnabled = useFlag('marketplaceIntegrations')
const {
data: schemas,
isSuccess: isSchemasLoaded,
@@ -59,37 +62,42 @@ export const WebhooksOverviewTab = () => {
)
}
return (
<IntegrationOverviewTab
actions={
isSchemasLoaded && isHooksEnabled ? null : (
<Admonition
showIcon={false}
type="default"
title="Enable database webhooks on your project"
>
<p>
Database Webhooks can be used to trigger serverless functions or send requests to an
HTTP endpoint
</p>
<ButtonTooltip
className="mt-2 w-fit"
onClick={() => enableHooksForProject()}
disabled={isEnablingHooks}
tooltip={{
content: {
side: 'bottom',
text: !canReadWebhooks
? 'You need additional permissions to enable webhooks'
: undefined,
},
}}
if (isMarketplaceEnabled) {
return <IntegrationOverviewTabV2 />
} else {
return (
<IntegrationOverviewTab
hideRequiredExtensionsSection
actions={
isSchemasLoaded && isHooksEnabled ? null : (
<Admonition
showIcon={false}
type="default"
title="Enable database webhooks on your project"
>
Enable webhooks
</ButtonTooltip>
</Admonition>
)
}
/>
)
<p>
Database Webhooks can be used to trigger serverless functions or send requests to an
HTTP endpoint
</p>
<ButtonTooltip
className="mt-2 w-fit"
onClick={() => enableHooksForProject()}
disabled={isEnablingHooks}
tooltip={{
content: {
side: 'bottom',
text: !canReadWebhooks
? 'You need additional permissions to enable webhooks'
: undefined,
},
}}
>
Enable webhooks
</ButtonTooltip>
</Admonition>
)
}
/>
)
}
}

View File

@@ -1,3 +1 @@
Database Webhooks allow you to send real-time data from your database to another system whenever a table event occurs.
You can hook into three table events: `INSERT`, `UPDATE`, and `DELETE`. All events are fired after a database row is changed.
Database Webhooks allow you to send real-time data from your database to another system whenever a table event occurs via an HTTP request or an Edge Function. You can hook into three table events: `INSERT`, `UPDATE`, and `DELETE`. These are fired after the row change is committed.

View File

@@ -31,6 +31,7 @@ export * from './sql/studio/database'
export * from './sql/studio/table-editor'
export * from './sql/studio/sql-editor'
export * from './sql/studio/role-impersonation'
export * from './sql/studio/integrations'
export default {
roles,

View File

@@ -0,0 +1,261 @@
// Copied over from Management API here:
// https://github.com/supabase/infrastructure/blob/develop/api/apps/mgmt-api/src/common/projects/project-database-webhooks.service.ts#L58
export const getCheckWebhooksEnabledSQL = () =>
`
SELECT EXISTS (
SELECT 1
FROM information_schema.schemata
WHERE schema_name = 'supabase_functions'
) AS schema_exists;
`.trim()
export const getEnableWebhooksSQL = () =>
`
BEGIN;
DO
$$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_roles
WHERE rolname = 'supabase_functions_admin'
)
THEN
CREATE USER supabase_functions_admin NOINHERIT CREATEROLE LOGIN NOREPLICATION;
END IF;
END
$$;
-- Event trigger for pg_net
CREATE OR REPLACE FUNCTION extensions.grant_pg_net_access()
RETURNS event_trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF EXISTS (
SELECT 1
FROM pg_event_trigger_ddl_commands() AS ev
JOIN pg_extension AS ext
ON ev.objid = ext.oid
WHERE ext.extname = 'pg_net'
)
THEN
GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres, anon, authenticated, service_role;
IF EXISTS (
SELECT FROM pg_extension
WHERE extname = 'pg_net'
-- all versions in use on existing projects as of 2025-02-20
-- version 0.12.0 onwards don't need these applied
AND extversion IN ('0.2', '0.6', '0.7', '0.7.1', '0.8', '0.10.0', '0.11.0')
) THEN
ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
REVOKE ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
REVOKE ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
GRANT EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
END IF;
END IF;
END;
$$;
COMMENT ON FUNCTION extensions.grant_pg_net_access IS 'Grants access to pg_net';
DO
$$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_event_trigger
WHERE evtname = 'issue_pg_net_access'
) THEN
CREATE EVENT TRIGGER issue_pg_net_access ON ddl_command_end WHEN TAG IN ('CREATE EXTENSION')
EXECUTE PROCEDURE extensions.grant_pg_net_access();
END IF;
END
$$;
-- pg_net grants when extension is already enabled
DO
$$
BEGIN
IF EXISTS (
SELECT 1
FROM pg_extension
WHERE extname = 'pg_net'
)
THEN
GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres, anon, authenticated, service_role;
IF EXISTS (
SELECT FROM pg_extension
WHERE extname = 'pg_net'
-- all versions in use on existing projects as of 2025-02-20
-- version 0.12.0 onwards don't need these applied
AND extversion IN ('0.2', '0.6', '0.7', '0.7.1', '0.8', '0.10.0', '0.11.0')
) THEN
ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
REVOKE ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
REVOKE ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
GRANT EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
END IF;
END IF;
END
$$;
-- Create pg_net extension
CREATE EXTENSION IF NOT EXISTS pg_net SCHEMA extensions;
-- Create supabase_functions schema
CREATE SCHEMA supabase_functions AUTHORIZATION supabase_admin;
GRANT USAGE ON SCHEMA supabase_functions TO postgres, anon, authenticated, service_role;
ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON TABLES TO postgres, anon, authenticated, service_role;
ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON FUNCTIONS TO postgres, anon, authenticated, service_role;
ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON SEQUENCES TO postgres, anon, authenticated, service_role;
-- supabase_functions.migrations definition
CREATE TABLE supabase_functions.migrations (
version text PRIMARY KEY,
inserted_at timestamptz NOT NULL DEFAULT NOW()
);
-- Initial supabase_functions migration
INSERT INTO supabase_functions.migrations (version) VALUES ('initial');
-- supabase_functions.hooks definition
CREATE TABLE supabase_functions.hooks (
id bigserial PRIMARY KEY,
hook_table_id integer NOT NULL,
hook_name text NOT NULL,
created_at timestamptz NOT NULL DEFAULT NOW(),
request_id bigint
);
CREATE INDEX supabase_functions_hooks_request_id_idx ON supabase_functions.hooks USING btree (request_id);
CREATE INDEX supabase_functions_hooks_h_table_id_h_name_idx ON supabase_functions.hooks USING btree (hook_table_id, hook_name);
COMMENT ON TABLE supabase_functions.hooks IS 'Supabase Functions Hooks: Audit trail for triggered hooks.';
CREATE FUNCTION supabase_functions.http_request()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
DECLARE
request_id bigint;
payload jsonb;
url text := TG_ARGV[0]::text;
method text := TG_ARGV[1]::text;
headers jsonb DEFAULT '{}'::jsonb;
params jsonb DEFAULT '{}'::jsonb;
timeout_ms integer DEFAULT 1000;
BEGIN
IF url IS NULL OR url = 'null' THEN
RAISE EXCEPTION 'url argument is missing';
END IF;
IF method IS NULL OR method = 'null' THEN
RAISE EXCEPTION 'method argument is missing';
END IF;
IF TG_ARGV[2] IS NULL OR TG_ARGV[2] = 'null' THEN
headers = '{"Content-Type": "application/json"}'::jsonb;
ELSE
headers = TG_ARGV[2]::jsonb;
END IF;
IF TG_ARGV[3] IS NULL OR TG_ARGV[3] = 'null' THEN
params = '{}'::jsonb;
ELSE
params = TG_ARGV[3]::jsonb;
END IF;
IF TG_ARGV[4] IS NULL OR TG_ARGV[4] = 'null' THEN
timeout_ms = 1000;
ELSE
timeout_ms = TG_ARGV[4]::integer;
END IF;
CASE
WHEN method = 'GET' THEN
SELECT http_get INTO request_id FROM net.http_get(
url,
params,
headers,
timeout_ms
);
WHEN method = 'POST' THEN
payload = jsonb_build_object(
'old_record', OLD,
'record', NEW,
'type', TG_OP,
'table', TG_TABLE_NAME,
'schema', TG_TABLE_SCHEMA
);
SELECT http_post INTO request_id FROM net.http_post(
url,
payload,
params,
headers,
timeout_ms
);
ELSE
RAISE EXCEPTION 'method argument % is invalid', method;
END CASE;
INSERT INTO supabase_functions.hooks
(hook_table_id, hook_name, request_id)
VALUES
(TG_RELID, TG_NAME, request_id);
RETURN NEW;
END
$function$;
GRANT ALL PRIVILEGES ON SCHEMA supabase_functions TO supabase_functions_admin;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA supabase_functions TO supabase_functions_admin;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA supabase_functions TO supabase_functions_admin;
ALTER USER supabase_functions_admin SET search_path = "supabase_functions";
ALTER table "supabase_functions".migrations OWNER TO supabase_functions_admin;
ALTER table "supabase_functions".hooks OWNER TO supabase_functions_admin;
ALTER function "supabase_functions".http_request() OWNER TO supabase_functions_admin;
GRANT supabase_functions_admin TO postgres;
-- Remove unused supabase_pg_net_admin role
DO
$$
BEGIN
IF EXISTS (
SELECT 1
FROM pg_roles
WHERE rolname = 'supabase_pg_net_admin'
)
THEN
REASSIGN OWNED BY supabase_pg_net_admin TO supabase_admin;
DROP OWNED BY supabase_pg_net_admin;
DROP ROLE supabase_pg_net_admin;
END IF;
END
$$;
INSERT INTO supabase_functions.migrations (version) VALUES ('20210809183423_update_grants');
ALTER function supabase_functions.http_request() SECURITY DEFINER;
ALTER function supabase_functions.http_request() SET search_path = supabase_functions;
REVOKE ALL ON FUNCTION supabase_functions.http_request() FROM PUBLIC;
GRANT EXECUTE ON FUNCTION supabase_functions.http_request() TO postgres, anon, authenticated, service_role;
COMMIT;
`.trim()

View File

@@ -0,0 +1 @@
export * from './enable-webhooks'